npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue2-booking-calendar

v2.8.0

Published

A customizable booking calendar component for Vue 2 with service, specialist, and customer management

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-calendar

Quick 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: false

Overnight schedule (e.g. 8 PM – 5 AM):

businessHoursStart: 20,
businessHoursEnd: 5,
is24HourOperation: false

24-hour operation:

is24HourOperation: true

Dropdown 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 time
  • brands — list of brand UUIDs that offer this service
  • branches — list of branch UUIDs where this service is available
  • specialists — 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 to
  • branch_uuid — single branch the specialist is assigned to
  • availability — 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) and showError(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