vue2-booking-calendar
v2.8.0
Published
A customizable booking calendar component for Vue 2 with service, specialist, and customer management
Maintainers
Readme
vue2-booking-calendar
A customizable booking calendar component for Vue 2 with service, specialist, and customer management.
Features
- Month, week, and day views
- Booking creation with bidirectional cascading dropdowns (brand ↔ branch ↔ service ↔ specialist ↔ customer)
- Select any field first — all other dropdowns filter automatically
- View booking details with full customer info (name, email, phone, address, notes)
- No-show and cancel booking with confirmation dialogs
- Specialist availability side panel with booked slot detection
- New customer inline creation from booking form
- Per-service duration with auto-computed end times
- Customizable business hours (standard, 24h, and overnight schedules)
- Event callbacks with custom function editors and live log
- Full JSON configuration for dropdown options and sample events
- Drag and drop events with 15-minute snap granularity
- SVG icons throughout
- Fully customizable via slots
Installation
npm install vue2-booking-calendarQuick Start
import Vue from 'vue'
import Vuex from 'vuex'
import BookingCalendar, { storeConfig } from 'vue2-booking-calendar'
import 'vue2-booking-calendar/dist/style.css'
Vue.use(Vuex)
Vue.use(BookingCalendar)
const store = new Vuex.Store(storeConfig)
new Vue({
store,
template: '<BookingCalendar />'
}).$mount('#app')Initializing with Custom Data
Pass your own brands, branches, services, specialists, and customers at setup:
const store = new Vuex.Store({
...storeConfig,
state() {
const state = storeConfig.state()
// Set your dropdown data
state.config.dropdownOptions.brand = [
{ uuid: 'b1', name: 'My Salon' },
{ uuid: 'b2', name: 'My Spa' }
]
state.config.dropdownOptions.branch = [
{ uuid: 'br1', brand_uuid: 'b1', name: 'Main Street' },
{ uuid: 'br2', brand_uuid: 'b1', name: 'Mall Location' },
{ uuid: 'br3', brand_uuid: 'b2', name: 'Downtown' }
]
state.config.dropdownOptions.service = [
{ uuid: 'sv1', name: 'Haircut', duration: 60, brands: ['b1'], branches: ['br1', 'br2'], specialists: ['sp1', 'sp2'] },
{ uuid: 'sv2', name: 'Massage', duration: 90, brands: ['b2'], branches: ['br3'], specialists: ['sp3'] }
]
state.config.dropdownOptions.specialist = [
{ uuid: 'sp1', name: 'Jane Smith', services: ['sv1'], brand_uuid: 'b1', branch_uuid: 'br1', availability: { monday: ['09:00','09:30','10:00'], tuesday: ['09:00','09:30'], wednesday: [], thursday: [], friday: [], saturday: [], sunday: [] } },
{ uuid: 'sp2', name: 'Bob Jones', services: ['sv1'], brand_uuid: 'b1', branch_uuid: 'br2', availability: { monday: ['10:00','10:30','11:00'], tuesday: [], wednesday: ['09:00','09:30'], thursday: [], friday: [], saturday: [], sunday: [] } },
{ uuid: 'sp3', name: 'Dr. Lee', services: ['sv2'], brand_uuid: 'b2', branch_uuid: 'br3', availability: { monday: ['14:00','14:30','15:00'], tuesday: ['14:00','14:30'], wednesday: [], thursday: [], friday: [], saturday: [], sunday: [] } }
]
state.config.dropdownOptions.customer = [
{ uuid: 'c1', name: 'Alice Brown', email: '[email protected]', phone: '+1 555-0101', gender: 'Female', address: '123 Oak St', notes: '' },
{ uuid: 'c2', name: 'Charlie Davis', email: '[email protected]', phone: '+1 555-0102', gender: 'Male', address: '', notes: 'VIP' }
]
state.config.dropdownOptions.time = [
'09:00', '09:30', '10:00', '10:30', '11:00', '11:30',
'12:00', '12:30', '13:00', '13:30', '14:00', '14:30',
'15:00', '15:30', '16:00', '16:30', '17:00'
]
// Clear sample events (start empty)
state.events = []
return state
}
})Loading Data from API
// After store is created, fetch and set data dynamically
async function loadCalendarData() {
const [brands, branches, services, specialists, customers] = await Promise.all([
fetch('/api/brands').then(r => r.json()),
fetch('/api/branches').then(r => r.json()),
fetch('/api/services').then(r => r.json()),
fetch('/api/specialists').then(r => r.json()),
fetch('/api/customers').then(r => r.json())
])
store.commit('SET_CONFIG', { key: 'dropdownOptions.brand', value: brands })
store.commit('SET_CONFIG', { key: 'dropdownOptions.branch', value: branches })
store.commit('SET_CONFIG', { key: 'dropdownOptions.service', value: services })
store.commit('SET_CONFIG', { key: 'dropdownOptions.specialist', value: specialists })
store.commit('SET_CONFIG', { key: 'dropdownOptions.customer', value: customers })
// Load existing bookings
const events = await fetch('/api/bookings').then(r => r.json())
store.commit('SET_EVENTS', events)
}
loadCalendarData()Configuration
All configuration is managed through the Vuex store config object. You can override defaults before creating the store:
const store = new Vuex.Store({
...storeConfig,
state() {
const state = storeConfig.state()
// Override config
state.config.businessHoursStart = 9
state.config.businessHoursEnd = 17
state.config.timeFormat = '24h'
state.config.showLogo = false
return state
}
})Config Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| slotDuration | number | 60 | Time slot duration in minutes (15, 30, 60) |
| businessHoursStart | number | 8 | Business hours start (0-23) |
| businessHoursEnd | number | 18 | Business hours end (0-23) |
| is24HourOperation | boolean | false | Enable 24-hour operation (disables business hours filtering) |
| timeFormat | string | '12h' | Time display format ('12h' or '24h') |
| firstDayOfWeek | number | 0 | First day of week (0 = Sunday, 1 = Monday) |
| showWeekends | boolean | true | Show weekend columns |
| maxVisibleEvents | number | 3 | Max events visible per day in month view |
| defaultEventDuration | number | 60 | Fallback duration when service has no duration |
| sidebarPosition | string | 'left' | Mini calendar sidebar position ('left', 'right', 'none') |
| showHeader | boolean | true | Show calendar navigation header |
| showLogo | boolean | true | Show the logo header bar |
| showCustomizationPanel | boolean | true | Show the customization settings panel |
| showManageButton | boolean | true | Show the Manage button in the view booking footer |
| showCompleteButton | boolean | true | Show the Complete action in the Actions dropdown |
| showCancelButton | boolean | true | Show the Cancel Booking action in the Actions dropdown |
| showViewEventModal | boolean | true | Show the view booking dialog when clicking an event (callback still fires when disabled) |
| maxVisibleEvents | number | 3 | Max events visible per hour slot in week/day views (overflow shows "+N more" badge) |
| allowPastDateBooking | boolean | false | Allow creating bookings on past dates |
| actionsPosition | string | 'left' | Actions dropdown position in footer ('left', 'center', 'right') |
| buttonRadius | string | 'rounded' | Button border-radius style ('square', 'rounded', 'pill') |
| showSelectCustomer | boolean | true | Show existing customer dropdown in booking form |
| showNewCustomer | boolean | true | Show new customer inline form in booking form |
| showCurrentTime | boolean | true | Show current time indicator line |
| highlightToday | boolean | true | Highlight today's column |
| eventBorderRadius | string | 'rounded' | Event card style ('rounded' or 'square') |
| dayStartHour | number | 0 | Hour to start the day view from |
Booking Field Visibility
Control which fields appear in the booking form and view modal:
bookingFields: {
brand: true, // Show in create/edit form
branch: true,
service: true,
date: true,
time: true,
specialist: true,
customer: true,
color: true
}
viewFields: {
brand: true, // Show in view booking modal
branch: true,
service: true,
date: true,
time: true,
specialist: true,
customer: true
}Business Hours
Standard schedule (e.g. 8 AM – 6 PM):
businessHoursStart: 8,
businessHoursEnd: 18,
is24HourOperation: falseOvernight schedule (e.g. 8 PM – 5 AM):
businessHoursStart: 20,
businessHoursEnd: 5,
is24HourOperation: false24-hour operation:
is24HourOperation: trueDropdown Data Formats
Brand
{ "uuid": "b1a2c3d4-...", "name": "RUSH Salon" }Branch
{
"uuid": "br1-0001-...",
"brand_uuid": "b1a2c3d4-...",
"name": "Downtown"
}Service
{
"uuid": "sv-0001",
"name": "Haircut",
"duration": 60,
"brands": ["b1a2c3d4-..."],
"branches": ["br1-0001-...", "br1-0002-..."],
"specialists": ["sp-0001", "sp-0002"]
}duration— service duration in minutes, used to auto-compute booking end timebrands— list of brand UUIDs that offer this servicebranches— list of branch UUIDs where this service is availablespecialists— list of specialist UUIDs who can perform this service
Specialist
{
"uuid": "sp-0001",
"name": "Maria Santos",
"services": ["sv-0001", "sv-0002"],
"brand_uuid": "b1a2c3d4-...",
"branch_uuid": "br1-0001-...",
"availability": {
"monday": ["09:00", "09:30", "10:00", "10:30", "11:00"],
"tuesday": ["09:00", "09:30", "10:00"],
"wednesday": [],
"thursday": ["14:00", "14:30", "15:00"],
"friday": ["09:00", "09:30", "10:00", "10:30"],
"saturday": [],
"sunday": []
}
}brand_uuid— single brand the specialist belongs tobranch_uuid— single branch the specialist is assigned toavailability— time slots per day of week (used by the availability side panel)- When a specialist + date are selected, the availability panel shows available/booked slots
Customer
{
"uuid": "cu-0001",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1 (555) 100-2001",
"gender": "Male",
"address": "123 Main St, New York, NY 10001",
"notes": "Prefers short fade on sides"
}All customer details are displayed in the view booking modal. New customers can be created inline from the booking form.
Customer Search & Selection
The booking form provides a searchable customer field:
- Search by name or mobile — type in the search box to filter customers in real-time
- Phone search — strips non-numeric characters for flexible matching (e.g. "555" matches "+1 (555) 100-2001")
- Customer details — when a customer is selected, their full details (name, phone, email, address, notes) are displayed in a read-only form below
- New customer creation — toggle to inline form with first name, last name, mobile, email
- Duplicate check — "Check Record" button searches existing customers by phone/email/name before creating a new one. If matches found, user can select an existing customer instead
Time Slots
["09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "12:00", ...]Time slots outside business hours are automatically disabled in the booking form.
Drag & Drop
Events can be dragged and dropped between time slots in week and day views:
- 15-minute snapping — drop position within the hour cell determines the minute offset (top quarter =
:00, second =:15, third =:30, bottom =:45) - Auto-collapse — clicking any event or empty cell automatically collapses all expanded "+N more" sections
- Duration preserved — the event's original duration is maintained after moving
Event Data Format
{
"id": "s1",
"title": "Haircut",
"brand": "b1a2c3d4-...",
"branch": "br1-0001-...",
"service": "sv-0001",
"specialist": "sp-0001",
"customer": "cu-0001",
"start": "2025-01-10T09:00:00.000Z",
"end": "2025-01-10T10:00:00.000Z",
"color": "#FF6B00"
}Callbacks
All callbacks can be toggled on/off and have customizable function bodies:
| Callback | Fires When | Payload |
|----------|-----------|---------|
| onEventCreate | New booking saved | { id, title, start, end } |
| onEventUpdate | Booking edited | { id, title, start, end } |
| onEventDelete | Booking deleted | { id } |
| onEventClick | Booking clicked/viewed | { id, title } |
| onDateChange | Calendar navigated | { date, direction } |
| onViewChange | View switched | { view } |
| onSlotClick | Empty slot clicked | { date, hour } |
| onEventDrop | Event drag & dropped | { id, from, to } |
| onSpecialistScheduleCheck | Availability panel loads | { specialist_uuid, specialist_name, date, day, available_slots, booked_slots, total_available, total_booked } |
| onBookingCancel | Cancel booking confirmed | { id, title, customer, specialist } |
| onBookingNoShow | No-show confirmed | { id, title, customer, specialist } |
| onBookingComplete | Booking marked as complete | { id, title, customer, specialist } |
Custom Callback Functions
Each callback has an editable function body that receives a payload argument:
callbackFns: {
onEventCreate: 'console.log("Event created:", payload)',
onSpecialistScheduleCheck: `
// Fetch real availability from API
fetch('/api/availability?specialist=' + payload.specialist_uuid + '&date=' + payload.date)
.then(r => r.json())
.then(data => console.log(data))
`
}Callback Examples
onEventCreate — Send new booking to API
callbackFns: {
onEventCreate: `
fetch('/api/bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(data => console.log('Booking saved:', data.id))
.catch(err => console.error('Failed to save:', err))
`
}onEventUpdate — Sync changes to backend
callbackFns: {
onEventUpdate: `
fetch('/api/bookings/' + payload.id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(() => console.log('Booking updated:', payload.id))
`
}onEventDelete — Delete from backend with confirmation log
callbackFns: {
onEventDelete: `
fetch('/api/bookings/' + payload.id, { method: 'DELETE' })
.then(() => console.log('Booking deleted:', payload.id))
`
}onEventClick — Track booking views in analytics
callbackFns: {
onEventClick: `
if (window.gtag) {
gtag('event', 'view_booking', {
booking_id: payload.id,
booking_title: payload.title
})
}
console.log('Viewed booking:', payload.title)
`
}onDateChange — Load events for the new date range
callbackFns: {
onDateChange: `
console.log('Calendar navigated:', payload.direction, 'to', payload.date)
// Fetch events for the new visible range
// fetch('/api/bookings?date=' + payload.date)
// .then(r => r.json())
// .then(events => store.commit('SET_EVENTS', events))
`
}onViewChange — Persist user's preferred view
callbackFns: {
onViewChange: `
localStorage.setItem('calendarView', payload.view)
console.log('View changed to:', payload.view)
`
}onSlotClick — Pre-fill booking from clicked slot
callbackFns: {
onSlotClick: `
console.log('Slot clicked:', payload.date, 'at', payload.hour + ':00')
`
}onEventDrop — Update booking after drag & drop
callbackFns: {
onEventDrop: `
fetch('/api/bookings/' + payload.id + '/reschedule', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ from: payload.from, to: payload.to })
})
.then(() => console.log('Booking rescheduled:', payload.id))
`
}onSpecialistScheduleCheck — Fetch real-time availability from API
callbackFns: {
onSpecialistScheduleCheck: `
console.log('Checking schedule for', payload.specialist_name, 'on', payload.date)
console.log('Day:', payload.day)
console.log('Available slots:', payload.total_available)
console.log('Booked slots:', payload.total_booked)
console.log('Available times:', payload.available_slots.join(', '))
console.log('Booked times:', payload.booked_slots.join(', '))
// Example: Fetch real availability from your API
// fetch('/api/specialists/' + payload.specialist_uuid + '/availability?date=' + payload.date)
// .then(r => r.json())
// .then(data => {
// // Update specialist availability dynamically
// console.log('Real availability:', data)
// })
`
}onBookingCancel — Handle booking cancellation
callbackFns: {
onBookingCancel: `
fetch('/api/bookings/' + payload.id + '/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: 'customer_request' })
})
.then(() => showSuccess('Booking cancelled successfully'))
.catch(err => showError('Failed to cancel: ' + err.message))
`
}onBookingNoShow — Record no-show and notify
callbackFns: {
onBookingNoShow: `
fetch('/api/bookings/' + payload.id + '/no-show', { method: 'POST' })
.then(() => showSuccess('No-show recorded for ' + payload.title))
.catch(err => showError('Failed to record no-show: ' + err.message))
`
}onBookingComplete — Mark service as delivered
callbackFns: {
onBookingComplete: `
fetch('/api/bookings/' + payload.id + '/complete', { method: 'POST' })
.then(() => showSuccess('Service completed: ' + payload.title))
.catch(err => showError('Failed to complete: ' + err.message))
`
}Note: All callback functions receive
showSuccess(message)andshowError(message)as arguments. Call these to display success (green) or error (red) toast notifications in the booking dialog. Toasts auto-dismiss after 4 seconds or can be clicked to dismiss.
Disabling Specific Callbacks
Toggle individual callbacks on/off:
// Via config
callbacks: {
onEventCreate: true, // enabled
onEventUpdate: true, // enabled
onEventDelete: false, // disabled
onEventClick: false, // disabled
onDateChange: true, // enabled
onViewChange: false, // disabled
onSlotClick: false, // disabled
onEventDrop: true, // enabled
onSpecialistScheduleCheck: true, // enabled
onBookingCancel: true, // enabled
onBookingNoShow: true, // enabled
onBookingComplete: true // enabled
}
// Or at runtime
store.commit('SET_CONFIG', { key: 'callbacks.onEventDelete', value: false })
store.commit('SET_CONFIG', { key: 'callbacks.onEventDrop', value: true })
store.commit('SET_CONFIG', { key: 'callbacks.onBookingCancel', value: false })
store.commit('SET_CONFIG', { key: 'callbacks.onBookingComplete', value: true })Updating Callback Functions at Runtime
// Update a callback function body
store.commit('SET_CONFIG', {
key: 'callbackFns.onEventCreate',
value: 'fetch("/api/bookings", { method: "POST", body: JSON.stringify(payload) })'
})
// Reset to default
store.commit('SET_CONFIG', {
key: 'callbackFns.onEventCreate',
value: 'console.log("Event created:", payload)'
})Vuex Store API
Mutations
| Mutation | Payload | Description |
|----------|---------|-------------|
| SET_DATE | Date | Set current calendar date |
| SET_VIEW | string | Set view ('month', 'week', 'day') |
| SET_EVENTS | array | Replace all events |
| ADD_EVENT | object | Add a single event |
| UPDATE_EVENT | object | Update event by id |
| DELETE_EVENT | string | Delete event by id |
| SET_CONFIG | { key, value } | Update config (supports dot notation: 'bookingFields.brand') |
| SHOW_EVENT_MODAL | boolean | Show/hide create/edit modal |
| SHOW_VIEW_MODAL | boolean | Show/hide view modal |
Actions
| Action | Payload | Description |
|--------|---------|-------------|
| next | — | Navigate to next period |
| prev | — | Navigate to previous period |
| today | — | Navigate to today |
| changeView | string | Switch view with callback |
| addEvent | object | Add event with callback |
| updateEvent | object | Update event with callback |
| deleteEvent | string | Delete event with callback |
| openCreateModal | slot? | Open create booking modal |
| openEditModal | event | Open edit booking modal |
| openViewModal | event | Open view booking modal |
| moveEvent | { id, newDate, newHour, newMinute } | Move event (drag & drop, snaps to 15-min intervals) |
| clearAllEvents | — | Remove all events |
| resetEvents | — | Reset to sample data |
Programmatic Config Updates
Update any config at runtime:
// Simple value
store.commit('SET_CONFIG', { key: 'timeFormat', value: '24h' })
// Nested value
store.commit('SET_CONFIG', { key: 'bookingFields.color', value: false })
// Replace dropdown options
store.commit('SET_CONFIG', {
key: 'dropdownOptions.brand',
value: [
{ uuid: 'b1', name: 'My Brand' },
{ uuid: 'b2', name: 'Other Brand' }
]
})
// Replace all events
store.commit('SET_EVENTS', myEventsArray)Custom Create/Edit Modal (Slots)
Replace the built-in booking modal with your own component using the create-modal slot:
<BookingCalendar>
<template #create-modal="{ form, event, close, isEdit }">
<MyCustomBookingDialog
v-if="$store.state.showEventModal"
:slot-data="form"
:event="event"
:is-edit="isEdit"
@close="close"
@save="handleSave"
/>
</template>
</BookingCalendar>Slot Props
| Prop | Type | Description |
|------|------|-------------|
| form | object\|null | The selected slot data ({ date, hour }) when creating |
| event | object\|null | The selected event object when editing |
| close | function | Call to close the modal |
| isEdit | boolean | true if editing, false if creating |
Available Internal Slots
The built-in EventModal also exposes these slots for partial customization:
| Slot | Props | Description |
|------|-------|-------------|
| header | { form, isEdit, close } | Replace the hero header |
| content | { form, isEdit } | Replace the entire form body (all cards) |
| before-fields | { form, isEdit } | Insert content before the form fields |
| after-fields | { form, isEdit } | Insert content after the form fields |
| footer | { form, isEdit, save, close, deleteEvent } | Replace the footer buttons |
<!-- Example: Add a note above the form -->
<BookingCalendar>
<template #create-modal="{ form, event, close, isEdit }">
<EventModal v-if="$store.state.showEventModal">
<template #before-fields>
<div class="my-note">Please fill all required fields</div>
</template>
<template #footer="{ save, close }">
<button @click="close">Cancel</button>
<button @click="save">Book Now</button>
</template>
</EventModal>
</template>
</BookingCalendar>License
MIT
