UNPKG

bookingjs

Version:

A framework-agnostic JavaScript library for managing rink bookings, availability, and scheduling

1 lines 186 kB
{"version":3,"file":"index.cjs","sources":["../src/utils/errors.js","../src/utils/validators.js","../src/utils/calculators.js","../src/models/Rink.js","../src/models/Green.js","../src/models/Booking.js","../src/models/BookingType.js","../src/models/AuditLogEntry.js","../src/models/TimeSlot.js","../src/models/BookingSystem.js"],"sourcesContent":["/**\r\n * Base error class for BookingJS\r\n */\r\nexport class BookingJSError extends Error {\r\n constructor(message, code) {\r\n super(message);\r\n this.name = this.constructor.name;\r\n this.code = code;\r\n Error.captureStackTrace(this, this.constructor);\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when a booking conflicts with an existing reservation\r\n */\r\nexport class BookingConflictError extends BookingJSError {\r\n constructor(message = 'Booking conflicts with existing reservation', conflictingBooking = null) {\r\n super(message, 'BOOKING_CONFLICT');\r\n this.conflictingBooking = conflictingBooking;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when requested time slot is not available\r\n */\r\nexport class InvalidTimeSlotError extends BookingJSError {\r\n constructor(message = 'Requested time slot is not available') {\r\n super(message, 'INVALID_TIME_SLOT');\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when member lacks required permissions\r\n */\r\nexport class MemberPermissionError extends BookingJSError {\r\n constructor(message = 'Member lacks required permissions', memberId = null) {\r\n super(message, 'MEMBER_PERMISSION');\r\n this.memberId = memberId;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown for data validation failures\r\n */\r\nexport class ValidationError extends BookingJSError {\r\n constructor(message = 'Data validation failed', field = null, value = null) {\r\n super(message, 'VALIDATION_ERROR');\r\n this.field = field;\r\n this.value = value;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when booking type is not allowed on green/rink\r\n */\r\nexport class BookingTypeNotAllowedError extends BookingJSError {\r\n constructor(message = 'Booking type is not allowed on this green/rink', bookingTypeId = null, location = null) {\r\n super(message, 'BOOKING_TYPE_NOT_ALLOWED');\r\n this.bookingTypeId = bookingTypeId;\r\n this.location = location;\r\n }\r\n} ","import { ValidationError, InvalidTimeSlotError, MemberPermissionError } from './errors.js';\r\n\r\n/**\r\n * Validates booking data\r\n * @param {Object} bookingData - Booking data to validate\r\n * @throws {ValidationError} When validation fails\r\n */\r\nexport function validateBooking(bookingData) {\r\n if (!bookingData) {\r\n throw new ValidationError('Booking data is required');\r\n }\r\n\r\n if (!bookingData.memberId) {\r\n throw new ValidationError('Member ID is required', 'memberId', bookingData.memberId);\r\n }\r\n\r\n if (!bookingData.rinkId) {\r\n throw new ValidationError('Rink ID is required', 'rinkId', bookingData.rinkId);\r\n }\r\n\r\n if (!bookingData.bookingTypeId) {\r\n throw new ValidationError('Booking type ID is required', 'bookingTypeId', bookingData.bookingTypeId);\r\n }\r\n\r\n if (!bookingData.date) {\r\n throw new ValidationError('Date is required', 'date', bookingData.date);\r\n }\r\n\r\n if (!bookingData.start || !(bookingData.start instanceof Date)) {\r\n throw new ValidationError('Valid start time is required', 'start', bookingData.start);\r\n }\r\n\r\n if (!bookingData.end || !(bookingData.end instanceof Date)) {\r\n throw new ValidationError('Valid end time is required', 'end', bookingData.end);\r\n }\r\n\r\n if (bookingData.start >= bookingData.end) {\r\n throw new ValidationError('End time must be after start time', 'time', {\r\n start: bookingData.start,\r\n end: bookingData.end\r\n });\r\n }\r\n\r\n // Validate status if provided\r\n const validStatuses = ['pending', 'confirmed', 'cancelled', 'completed'];\r\n if (bookingData.status && !validStatuses.includes(bookingData.status)) {\r\n throw new ValidationError('Invalid booking status', 'status', bookingData.status);\r\n }\r\n}\r\n\r\n/**\r\n * Validates time slot data\r\n * @param {Object} timeSlot - Time slot to validate\r\n * @throws {InvalidTimeSlotError} When validation fails\r\n */\r\nexport function validateTimeSlot(timeSlot) {\r\n if (!timeSlot) {\r\n throw new InvalidTimeSlotError('Time slot is required');\r\n }\r\n\r\n if (!timeSlot.start || !(timeSlot.start instanceof Date)) {\r\n throw new InvalidTimeSlotError('Valid start time is required');\r\n }\r\n\r\n if (!timeSlot.end || !(timeSlot.end instanceof Date)) {\r\n throw new InvalidTimeSlotError('Valid end time is required');\r\n }\r\n\r\n if (timeSlot.start >= timeSlot.end) {\r\n throw new InvalidTimeSlotError('End time must be after start time');\r\n }\r\n}\r\n\r\n/**\r\n * Validates member permissions (placeholder for future implementation)\r\n * @param {string} memberId - Member ID to validate\r\n * @param {string} action - Action being performed\r\n * @throws {MemberPermissionError} When validation fails\r\n */\r\nexport function validateMember(memberId, action = 'access') {\r\n if (!memberId || typeof memberId !== 'string') {\r\n throw new MemberPermissionError('Valid member ID is required', memberId);\r\n }\r\n\r\n // Placeholder for future permission validation logic\r\n // This would integrate with the container app's permission system\r\n}\r\n\r\n/**\r\n * Validates UUID format\r\n * @param {string} id - ID to validate\r\n * @param {string} fieldName - Name of the field being validated\r\n * @throws {ValidationError} When validation fails\r\n */\r\nexport function validateUUID(id, fieldName = 'id') {\r\n const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\r\n \r\n if (!id || typeof id !== 'string' || !uuidRegex.test(id)) {\r\n throw new ValidationError(`Valid UUID required for ${fieldName}`, fieldName, id);\r\n }\r\n}\r\n\r\n/**\r\n * Validates color hex format\r\n * @param {string} color - Color hex string to validate\r\n * @param {string} fieldName - Name of the field being validated\r\n * @throws {ValidationError} When validation fails\r\n */\r\nexport function validateColor(color, fieldName = 'color') {\r\n const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;\r\n \r\n if (!color || typeof color !== 'string' || !colorRegex.test(color)) {\r\n throw new ValidationError(`Valid hex color required for ${fieldName}`, fieldName, color);\r\n }\r\n} ","/**\r\n * Calculate duration between two dates in minutes\r\n * @param {Date} start - Start time\r\n * @param {Date} end - End time\r\n * @returns {number} Duration in minutes\r\n */\r\nexport function calculateDuration(start, end) {\r\n if (!start || !end || !(start instanceof Date) || !(end instanceof Date)) {\r\n return 0;\r\n }\r\n return Math.round((end - start) / (1000 * 60));\r\n}\r\n\r\n/**\r\n * Check if a time slot is available (not overlapping with existing bookings)\r\n * @param {Date} start - Proposed start time\r\n * @param {Date} end - Proposed end time\r\n * @param {Array} existingBookings - Array of existing bookings to check against\r\n * @param {string} excludeBookingId - Booking ID to exclude from check (for updates)\r\n * @returns {boolean} True if time slot is available\r\n */\r\nexport function isTimeSlotAvailable(start, end, existingBookings = [], excludeBookingId = null) {\r\n if (!start || !end || !(start instanceof Date) || !(end instanceof Date)) {\r\n return false;\r\n }\r\n\r\n if (start >= end) {\r\n return false;\r\n }\r\n\r\n for (const booking of existingBookings) {\r\n // Skip cancelled bookings and excluded booking\r\n if (booking.status === 'cancelled' || booking._id === excludeBookingId) {\r\n continue;\r\n }\r\n\r\n const bookingStart = booking.start;\r\n const bookingEnd = booking.end;\r\n\r\n // Check for time overlap\r\n if (start < bookingEnd && end > bookingStart) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n}\r\n\r\n/**\r\n * Format date and time for display\r\n * @param {Date} date - Date to format\r\n * @param {Object} options - Formatting options\r\n * @returns {string} Formatted date string\r\n */\r\nexport function formatDateTime(date, options = {}) {\r\n if (!date || !(date instanceof Date)) {\r\n return '';\r\n }\r\n\r\n const defaults = {\r\n year: 'numeric',\r\n month: '2-digit',\r\n day: '2-digit',\r\n hour: '2-digit',\r\n minute: '2-digit',\r\n hour12: false\r\n };\r\n\r\n const formatOptions = { ...defaults, ...options };\r\n return date.toLocaleString('en-GB', formatOptions);\r\n}\r\n\r\n/**\r\n * Check if two time periods overlap\r\n * @param {Date} start1 - Start of first period\r\n * @param {Date} end1 - End of first period \r\n * @param {Date} start2 - Start of second period\r\n * @param {Date} end2 - End of second period\r\n * @returns {boolean} True if periods overlap\r\n */\r\nexport function timePeriodsOverlap(start1, end1, start2, end2) {\r\n return start1 < end2 && end1 > start2;\r\n}\r\n\r\n/**\r\n * Get the next available time slot after a given time\r\n * @param {Date} afterTime - Time to search after\r\n * @param {number} duration - Duration in minutes\r\n * @param {Array} existingBookings - Existing bookings to avoid\r\n * @param {Object} workingHours - Working hours constraints\r\n * @returns {Object|null} Next available slot or null\r\n */\r\nexport function getNextAvailableSlot(afterTime, duration, existingBookings = [], workingHours = null) {\r\n if (!afterTime || !duration || duration <= 0) {\r\n return null;\r\n }\r\n\r\n const proposedStart = new Date(afterTime);\r\n const proposedEnd = new Date(proposedStart.getTime() + (duration * 60 * 1000));\r\n\r\n // Apply working hours if provided\r\n if (workingHours) {\r\n const startHour = proposedStart.getHours();\r\n const endHour = proposedEnd.getHours();\r\n \r\n if (startHour < workingHours.start || endHour > workingHours.end) {\r\n // Adjust to next available working hour\r\n const nextDay = new Date(proposedStart);\r\n nextDay.setDate(nextDay.getDate() + 1);\r\n nextDay.setHours(workingHours.start, 0, 0, 0);\r\n return getNextAvailableSlot(nextDay, duration, existingBookings, workingHours);\r\n }\r\n }\r\n\r\n if (isTimeSlotAvailable(proposedStart, proposedEnd, existingBookings)) {\r\n return {\r\n start: proposedStart,\r\n end: proposedEnd\r\n };\r\n }\r\n\r\n // Find next gap in bookings\r\n const futureBookings = existingBookings\r\n .filter(booking => booking.start > afterTime && booking.status !== 'cancelled')\r\n .sort((a, b) => a.start - b.start);\r\n\r\n if (futureBookings.length === 0) {\r\n return {\r\n start: proposedStart,\r\n end: proposedEnd\r\n };\r\n }\r\n\r\n // Try after each booking\r\n for (const booking of futureBookings) {\r\n const nextSlot = getNextAvailableSlot(booking.end, duration, existingBookings, workingHours);\r\n if (nextSlot) {\r\n return nextSlot;\r\n }\r\n }\r\n\r\n return null;\r\n} ","import { v4 as uuidv4 } from 'uuid';\r\nimport { ValidationError, BookingTypeNotAllowedError } from '../utils/errors.js';\r\nimport { isTimeSlotAvailable } from '../utils/calculators.js';\r\n\r\n/**\r\n * Represents a bookable rink within a green\r\n */\r\nexport class Rink {\r\n /**\r\n * Create a new Rink\r\n * @param {Object} data - Rink configuration\r\n * @param {string} [data._id] - Unique identifier (auto-generated if not provided)\r\n * @param {number} data.number - Rink number (1-12)\r\n * @param {Array<string>} [data.allowedBookingTypes] - Array of BookingType IDs that can be booked\r\n */\r\n constructor(data) {\r\n if (!data || typeof data !== 'object') {\r\n throw new ValidationError('Rink data is required');\r\n }\r\n\r\n if (typeof data.number !== 'number' || data.number < 1 || data.number > 12) {\r\n throw new ValidationError('Rink number must be between 1 and 12', 'number', data.number);\r\n }\r\n\r\n this._id = data._id || uuidv4();\r\n this.number = data.number;\r\n this.allowedBookingTypes = Array.isArray(data.allowedBookingTypes) \r\n ? [...data.allowedBookingTypes] \r\n : [];\r\n }\r\n\r\n /**\r\n * Update rink details\r\n * @param {Object} updates - Properties to update\r\n * @returns {Rink} This instance for chaining\r\n */\r\n update(updates) {\r\n if (!updates || typeof updates !== 'object') {\r\n throw new ValidationError('Updates object is required');\r\n }\r\n\r\n const allowedFields = ['number', 'allowedBookingTypes'];\r\n const updateKeys = Object.keys(updates);\r\n\r\n for (const key of updateKeys) {\r\n if (!allowedFields.includes(key)) {\r\n throw new ValidationError(`Field '${key}' cannot be updated`, key, updates[key]);\r\n }\r\n }\r\n\r\n // Validate number update\r\n if (updates.number !== undefined) {\r\n if (typeof updates.number !== 'number' || updates.number < 1 || updates.number > 12) {\r\n throw new ValidationError('Rink number must be between 1 and 12', 'number', updates.number);\r\n }\r\n this.number = updates.number;\r\n }\r\n\r\n // Validate allowedBookingTypes update\r\n if (updates.allowedBookingTypes !== undefined) {\r\n if (!Array.isArray(updates.allowedBookingTypes)) {\r\n throw new ValidationError('Allowed booking types must be an array', 'allowedBookingTypes', updates.allowedBookingTypes);\r\n }\r\n this.allowedBookingTypes = [...updates.allowedBookingTypes];\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Check availability for specific time\r\n * @param {Date} date - Date to check\r\n * @param {Date} start - Start time to check\r\n * @param {Date} end - End time to check\r\n * @param {Array} existingBookings - Existing bookings to check against\r\n * @param {string} excludeBookingId - Booking ID to exclude from check\r\n * @returns {boolean} True if available\r\n */\r\n isAvailable(date, start, end, existingBookings = [], excludeBookingId = null) {\r\n if (!date || !start || !end) {\r\n return false;\r\n }\r\n\r\n // Filter bookings for this rink and date\r\n const rinkBookings = existingBookings.filter(booking => \r\n booking.rinkId === this._id &&\r\n booking.date.toDateString() === date.toDateString() &&\r\n booking.status !== 'cancelled'\r\n );\r\n\r\n return isTimeSlotAvailable(start, end, rinkBookings, excludeBookingId);\r\n }\r\n\r\n /**\r\n * Add a booking type to allowed types\r\n * @param {string} bookingTypeId - Booking type ID to add\r\n * @returns {Rink} This instance for chaining\r\n */\r\n addAllowedBookingType(bookingTypeId) {\r\n if (!bookingTypeId || typeof bookingTypeId !== 'string') {\r\n throw new ValidationError('Valid booking type ID is required', 'bookingTypeId', bookingTypeId);\r\n }\r\n\r\n if (!this.allowedBookingTypes.includes(bookingTypeId)) {\r\n this.allowedBookingTypes.push(bookingTypeId);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove a booking type from allowed types\r\n * @param {string} bookingTypeId - Booking type ID to remove\r\n * @returns {Rink} This instance for chaining\r\n */\r\n removeAllowedBookingType(bookingTypeId) {\r\n if (!bookingTypeId || typeof bookingTypeId !== 'string') {\r\n throw new ValidationError('Valid booking type ID is required', 'bookingTypeId', bookingTypeId);\r\n }\r\n\r\n const index = this.allowedBookingTypes.indexOf(bookingTypeId);\r\n if (index > -1) {\r\n this.allowedBookingTypes.splice(index, 1);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Check if booking type is allowed on this rink\r\n * @param {string} bookingTypeId - Booking type ID to check\r\n * @returns {boolean} True if booking type is allowed\r\n */\r\n isBookingTypeAllowed(bookingTypeId) {\r\n if (!bookingTypeId || typeof bookingTypeId !== 'string') {\r\n return false;\r\n }\r\n\r\n // If no restrictions, allow all booking types\r\n if (this.allowedBookingTypes.length === 0) {\r\n return true;\r\n }\r\n\r\n return this.allowedBookingTypes.includes(bookingTypeId);\r\n }\r\n\r\n /**\r\n * Validate if a booking can be made on this rink\r\n * @param {string} bookingTypeId - Booking type ID\r\n * @param {Date} date - Booking date\r\n * @param {Date} start - Start time\r\n * @param {Date} end - End time\r\n * @param {Array} existingBookings - Existing bookings\r\n * @param {string} excludeBookingId - Booking ID to exclude\r\n * @throws {BookingTypeNotAllowedError} When booking type is not allowed\r\n * @throws {ValidationError} When time slot is not available\r\n */\r\n validateBooking(bookingTypeId, date, start, end, existingBookings = [], excludeBookingId = null) {\r\n // Check if booking type is allowed\r\n if (!this.isBookingTypeAllowed(bookingTypeId)) {\r\n throw new BookingTypeNotAllowedError(\r\n `Booking type ${bookingTypeId} is not allowed on rink ${this.number}`,\r\n bookingTypeId,\r\n `rink-${this._id}`\r\n );\r\n }\r\n\r\n // Check availability\r\n if (!this.isAvailable(date, start, end, existingBookings, excludeBookingId)) {\r\n throw new ValidationError(\r\n `Rink ${this.number} is not available for the requested time slot`,\r\n 'availability',\r\n { date, start, end }\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Get bookings for this rink within a date range\r\n * @param {Array} allBookings - All bookings to filter\r\n * @param {Date} startDate - Start of date range\r\n * @param {Date} endDate - End of date range\r\n * @returns {Array} Filtered bookings for this rink\r\n */\r\n getBookings(allBookings, startDate = null, endDate = null) {\r\n let rinkBookings = allBookings.filter(booking => booking.rinkId === this._id);\r\n\r\n if (startDate && endDate) {\r\n rinkBookings = rinkBookings.filter(booking => {\r\n const bookingDate = new Date(booking.date);\r\n return bookingDate >= startDate && bookingDate <= endDate;\r\n });\r\n }\r\n\r\n return rinkBookings.sort((a, b) => a.start - b.start);\r\n }\r\n\r\n /**\r\n * Get utilization statistics for this rink\r\n * @param {Array} allBookings - All bookings to analyze\r\n * @param {Date} startDate - Start of analysis period\r\n * @param {Date} endDate - End of analysis period\r\n * @returns {Object} Utilization statistics\r\n */\r\n getUtilizationStats(allBookings, startDate, endDate) {\r\n const rinkBookings = this.getBookings(allBookings, startDate, endDate)\r\n .filter(booking => booking.status === 'confirmed' || booking.status === 'completed');\r\n\r\n const totalMinutes = rinkBookings.reduce((sum, booking) => {\r\n return sum + Math.round((booking.end - booking.start) / (1000 * 60));\r\n }, 0);\r\n\r\n const totalDays = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));\r\n const totalAvailableMinutes = totalDays * 12 * 60; // Assuming 12 hours per day\r\n\r\n return {\r\n rinkId: this._id,\r\n rinkNumber: this.number,\r\n totalBookings: rinkBookings.length,\r\n totalBookedMinutes: totalMinutes,\r\n totalAvailableMinutes,\r\n utilizationPercentage: (totalMinutes / totalAvailableMinutes) * 100,\r\n averageBookingDuration: rinkBookings.length > 0 ? totalMinutes / rinkBookings.length : 0\r\n };\r\n }\r\n\r\n /**\r\n * Convert to JSON string for persistence\r\n * @returns {string} JSON string representation\r\n */\r\n toJSON() {\r\n const data = {\r\n _id: this._id,\r\n number: this.number,\r\n allowedBookingTypes: [...this.allowedBookingTypes]\r\n };\r\n return JSON.stringify(data);\r\n }\r\n\r\n /**\r\n * Create Rink instance from JSON string\r\n * @param {string} jsonString - JSON string data\r\n * @returns {Rink} New Rink instance\r\n */\r\n static fromJSON(jsonString) {\r\n if (!jsonString || typeof jsonString !== 'string') {\r\n throw new ValidationError('Valid JSON string is required');\r\n }\r\n\r\n let jsonData;\r\n try {\r\n jsonData = JSON.parse(jsonString);\r\n } catch (error) {\r\n throw new ValidationError('Invalid JSON string format');\r\n }\r\n\r\n return new Rink({\r\n _id: jsonData._id,\r\n number: jsonData.number,\r\n allowedBookingTypes: jsonData.allowedBookingTypes || []\r\n });\r\n }\r\n} ","import { v4 as uuidv4 } from 'uuid';\r\nimport { ValidationError, BookingTypeNotAllowedError } from '../utils/errors.js';\r\nimport { Rink } from './Rink.js';\r\n\r\n/**\r\n * Represents a bowling green containing multiple rinks\r\n */\r\nexport class Green {\r\n /**\r\n * Create a new Green\r\n * @param {Object} data - Green configuration\r\n * @param {string} [data._id] - Unique identifier (auto-generated if not provided)\r\n * @param {string} data.name - Name of the green\r\n * @param {Array<Rink>} [data.rinks] - Array of Rink instances (max 12)\r\n * @param {string} [data.location] - Physical location/address\r\n * @param {Array<string>} [data.facilities] - Array of available facilities\r\n * @param {Array<string>} [data.allowedBookingTypes] - Array of BookingType IDs\r\n */\r\n constructor(data) {\r\n if (!data || typeof data !== 'object') {\r\n throw new ValidationError('Green data is required');\r\n }\r\n\r\n if (!data.name || typeof data.name !== 'string') {\r\n throw new ValidationError('Name is required', 'name', data.name);\r\n }\r\n\r\n this._id = data._id || uuidv4();\r\n this.name = data.name.trim();\r\n this.location = data.location ? data.location.trim() : '';\r\n this.facilities = Array.isArray(data.facilities) ? [...data.facilities] : [];\r\n this.allowedBookingTypes = Array.isArray(data.allowedBookingTypes) \r\n ? [...data.allowedBookingTypes] \r\n : [];\r\n\r\n // Initialize rinks\r\n this.rinks = [];\r\n if (Array.isArray(data.rinks)) {\r\n for (const rinkData of data.rinks) {\r\n if (rinkData instanceof Rink) {\r\n this.addRink(rinkData);\r\n } else {\r\n this.addRink(new Rink(rinkData));\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Add a rink to the green (max 12)\r\n * @param {Rink|Object} rink - Rink instance or rink data\r\n * @returns {Green} This instance for chaining\r\n */\r\n addRink(rink) {\r\n if (this.rinks.length >= 12) {\r\n throw new ValidationError('Maximum of 12 rinks allowed per green', 'rinks', this.rinks.length);\r\n }\r\n\r\n let rinkInstance;\r\n if (rink instanceof Rink) {\r\n rinkInstance = rink;\r\n } else if (rink && typeof rink === 'object') {\r\n rinkInstance = new Rink(rink);\r\n } else {\r\n throw new ValidationError('Valid rink data or Rink instance is required', 'rink', rink);\r\n }\r\n\r\n // Check for duplicate rink numbers\r\n const existingRink = this.rinks.find(r => r.number === rinkInstance.number);\r\n if (existingRink) {\r\n throw new ValidationError(\r\n `Rink number ${rinkInstance.number} already exists`,\r\n 'rinkNumber',\r\n rinkInstance.number\r\n );\r\n }\r\n\r\n this.rinks.push(rinkInstance);\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove a rink from the green\r\n * @param {string} rinkId - Rink ID to remove\r\n * @returns {Green} This instance for chaining\r\n */\r\n removeRink(rinkId) {\r\n if (!rinkId || typeof rinkId !== 'string') {\r\n throw new ValidationError('Valid rink ID is required', 'rinkId', rinkId);\r\n }\r\n\r\n const index = this.rinks.findIndex(rink => rink._id === rinkId);\r\n if (index === -1) {\r\n throw new ValidationError('Rink not found', 'rinkId', rinkId);\r\n }\r\n\r\n this.rinks.splice(index, 1);\r\n return this;\r\n }\r\n\r\n /**\r\n * Get a specific rink by ID\r\n * @param {string} rinkId - Rink ID to find\r\n * @returns {Rink|null} Rink instance or null if not found\r\n */\r\n getRink(rinkId) {\r\n if (!rinkId || typeof rinkId !== 'string') {\r\n return null;\r\n }\r\n\r\n return this.rinks.find(rink => rink._id === rinkId) || null;\r\n }\r\n\r\n /**\r\n * Get a rink by number\r\n * @param {number} rinkNumber - Rink number to find\r\n * @returns {Rink|null} Rink instance or null if not found\r\n */\r\n getRinkByNumber(rinkNumber) {\r\n if (typeof rinkNumber !== 'number') {\r\n return null;\r\n }\r\n\r\n return this.rinks.find(rink => rink.number === rinkNumber) || null;\r\n }\r\n\r\n /**\r\n * Get available rinks for time period\r\n * @param {Date} date - Date to check\r\n * @param {Date} start - Start time\r\n * @param {Date} end - End time\r\n * @param {Array} existingBookings - Existing bookings to check against\r\n * @param {string} bookingTypeId - Booking type ID to check restrictions\r\n * @param {string} excludeBookingId - Booking ID to exclude from check\r\n * @returns {Array<Rink>} Available rinks\r\n */\r\n getAvailableRinks(date, start, end, existingBookings = [], bookingTypeId = null, excludeBookingId = null) {\r\n if (!date || !start || !end) {\r\n return [];\r\n }\r\n\r\n return this.rinks.filter(rink => {\r\n // Check booking type restrictions\r\n if (bookingTypeId && !this.isBookingTypeAllowed(bookingTypeId)) {\r\n return false;\r\n }\r\n \r\n if (bookingTypeId && !rink.isBookingTypeAllowed(bookingTypeId)) {\r\n return false;\r\n }\r\n\r\n // Check availability\r\n return rink.isAvailable(date, start, end, existingBookings, excludeBookingId);\r\n }).sort((a, b) => a.number - b.number);\r\n }\r\n\r\n /**\r\n * Add a booking type to allowed types\r\n * @param {string} bookingTypeId - Booking type ID to add\r\n * @returns {Green} This instance for chaining\r\n */\r\n addAllowedBookingType(bookingTypeId) {\r\n if (!bookingTypeId || typeof bookingTypeId !== 'string') {\r\n throw new ValidationError('Valid booking type ID is required', 'bookingTypeId', bookingTypeId);\r\n }\r\n\r\n if (!this.allowedBookingTypes.includes(bookingTypeId)) {\r\n this.allowedBookingTypes.push(bookingTypeId);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove a booking type from allowed types\r\n * @param {string} bookingTypeId - Booking type ID to remove\r\n * @returns {Green} This instance for chaining\r\n */\r\n removeAllowedBookingType(bookingTypeId) {\r\n if (!bookingTypeId || typeof bookingTypeId !== 'string') {\r\n throw new ValidationError('Valid booking type ID is required', 'bookingTypeId', bookingTypeId);\r\n }\r\n\r\n const index = this.allowedBookingTypes.indexOf(bookingTypeId);\r\n if (index > -1) {\r\n this.allowedBookingTypes.splice(index, 1);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Check if booking type is allowed on this green\r\n * @param {string} bookingTypeId - Booking type ID to check\r\n * @returns {boolean} True if booking type is allowed\r\n */\r\n isBookingTypeAllowed(bookingTypeId) {\r\n if (!bookingTypeId || typeof bookingTypeId !== 'string') {\r\n return false;\r\n }\r\n\r\n // If no restrictions, allow all booking types\r\n if (this.allowedBookingTypes.length === 0) {\r\n return true;\r\n }\r\n\r\n return this.allowedBookingTypes.includes(bookingTypeId);\r\n }\r\n\r\n /**\r\n * Add a facility to the green\r\n * @param {string} facility - Facility name to add\r\n * @returns {Green} This instance for chaining\r\n */\r\n addFacility(facility) {\r\n if (!facility || typeof facility !== 'string') {\r\n throw new ValidationError('Valid facility name is required', 'facility', facility);\r\n }\r\n\r\n const facilityName = facility.trim();\r\n if (!this.facilities.includes(facilityName)) {\r\n this.facilities.push(facilityName);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove a facility from the green\r\n * @param {string} facility - Facility name to remove\r\n * @returns {Green} This instance for chaining\r\n */\r\n removeFacility(facility) {\r\n if (!facility || typeof facility !== 'string') {\r\n throw new ValidationError('Valid facility name is required', 'facility', facility);\r\n }\r\n\r\n const index = this.facilities.indexOf(facility.trim());\r\n if (index > -1) {\r\n this.facilities.splice(index, 1);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Update green details\r\n * @param {Object} updates - Properties to update\r\n * @returns {Green} This instance for chaining\r\n */\r\n update(updates) {\r\n if (!updates || typeof updates !== 'object') {\r\n throw new ValidationError('Updates object is required');\r\n }\r\n\r\n const allowedFields = ['name', 'location', 'facilities', 'allowedBookingTypes'];\r\n const updateKeys = Object.keys(updates);\r\n\r\n for (const key of updateKeys) {\r\n if (!allowedFields.includes(key)) {\r\n throw new ValidationError(`Field '${key}' cannot be updated`, key, updates[key]);\r\n }\r\n }\r\n\r\n // Apply updates\r\n if (updates.name !== undefined) {\r\n if (!updates.name || typeof updates.name !== 'string') {\r\n throw new ValidationError('Name must be a non-empty string', 'name', updates.name);\r\n }\r\n this.name = updates.name.trim();\r\n }\r\n\r\n if (updates.location !== undefined) {\r\n this.location = updates.location ? updates.location.trim() : '';\r\n }\r\n\r\n if (updates.facilities !== undefined) {\r\n if (!Array.isArray(updates.facilities)) {\r\n throw new ValidationError('Facilities must be an array', 'facilities', updates.facilities);\r\n }\r\n this.facilities = [...updates.facilities];\r\n }\r\n\r\n if (updates.allowedBookingTypes !== undefined) {\r\n if (!Array.isArray(updates.allowedBookingTypes)) {\r\n throw new ValidationError('Allowed booking types must be an array', 'allowedBookingTypes', updates.allowedBookingTypes);\r\n }\r\n this.allowedBookingTypes = [...updates.allowedBookingTypes];\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get bookings for this green within a date range\r\n * @param {Array} allBookings - All bookings to filter\r\n * @param {Date} startDate - Start of date range\r\n * @param {Date} endDate - End of date range\r\n * @returns {Array} Filtered bookings for this green\r\n */\r\n getBookings(allBookings, startDate = null, endDate = null) {\r\n const rinkIds = this.rinks.map(rink => rink._id);\r\n let greenBookings = allBookings.filter(booking => rinkIds.includes(booking.rinkId));\r\n\r\n if (startDate && endDate) {\r\n greenBookings = greenBookings.filter(booking => {\r\n const bookingDate = new Date(booking.date);\r\n return bookingDate >= startDate && bookingDate <= endDate;\r\n });\r\n }\r\n\r\n return greenBookings.sort((a, b) => a.start - b.start);\r\n }\r\n\r\n /**\r\n * Get utilization statistics for this green\r\n * @param {Array} allBookings - All bookings to analyze\r\n * @param {Date} startDate - Start of analysis period\r\n * @param {Date} endDate - End of analysis period\r\n * @returns {Object} Utilization statistics\r\n */\r\n getUtilizationStats(allBookings, startDate, endDate) {\r\n const greenBookings = this.getBookings(allBookings, startDate, endDate)\r\n .filter(booking => booking.status === 'confirmed' || booking.status === 'completed');\r\n\r\n const totalMinutes = greenBookings.reduce((sum, booking) => {\r\n return sum + Math.round((booking.end - booking.start) / (1000 * 60));\r\n }, 0);\r\n\r\n const totalDays = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));\r\n const totalAvailableMinutes = totalDays * this.rinks.length * 12 * 60; // Assuming 12 hours per day per rink\r\n\r\n const rinkStats = this.rinks.map(rink => \r\n rink.getUtilizationStats(allBookings, startDate, endDate)\r\n );\r\n\r\n return {\r\n greenId: this._id,\r\n greenName: this.name,\r\n totalRinks: this.rinks.length,\r\n totalBookings: greenBookings.length,\r\n totalBookedMinutes: totalMinutes,\r\n totalAvailableMinutes,\r\n utilizationPercentage: (totalMinutes / totalAvailableMinutes) * 100,\r\n averageBookingDuration: greenBookings.length > 0 ? totalMinutes / greenBookings.length : 0,\r\n rinkStats\r\n };\r\n }\r\n\r\n /**\r\n * Validate if a booking can be made on this green\r\n * @param {string} rinkId - Rink ID\r\n * @param {string} bookingTypeId - Booking type ID\r\n * @param {Date} date - Booking date\r\n * @param {Date} start - Start time\r\n * @param {Date} end - End time\r\n * @param {Array} existingBookings - Existing bookings\r\n * @param {string} excludeBookingId - Booking ID to exclude\r\n * @throws {ValidationError} When rink is not found\r\n * @throws {BookingTypeNotAllowedError} When booking type is not allowed\r\n */\r\n validateBooking(rinkId, bookingTypeId, date, start, end, existingBookings = [], excludeBookingId = null) {\r\n // Check if rink exists\r\n const rink = this.getRink(rinkId);\r\n if (!rink) {\r\n throw new ValidationError(`Rink ${rinkId} not found in green ${this.name}`, 'rinkId', rinkId);\r\n }\r\n\r\n // Check if booking type is allowed on green\r\n if (!this.isBookingTypeAllowed(bookingTypeId)) {\r\n throw new BookingTypeNotAllowedError(\r\n `Booking type ${bookingTypeId} is not allowed on green ${this.name}`,\r\n bookingTypeId,\r\n `green-${this._id}`\r\n );\r\n }\r\n\r\n // Delegate to rink validation\r\n rink.validateBooking(bookingTypeId, date, start, end, existingBookings, excludeBookingId);\r\n }\r\n\r\n /**\r\n * Convert to JSON string for persistence\r\n * @returns {string} JSON string representation\r\n */\r\n toJSON() {\r\n const data = {\r\n _id: this._id,\r\n name: this.name,\r\n location: this.location,\r\n facilities: [...this.facilities],\r\n allowedBookingTypes: [...this.allowedBookingTypes],\r\n rinks: this.rinks.map(rink => JSON.parse(rink.toJSON()))\r\n };\r\n return JSON.stringify(data);\r\n }\r\n\r\n /**\r\n * Create Green instance from JSON string\r\n * @param {string} jsonString - JSON string data\r\n * @returns {Green} New Green instance\r\n */\r\n static fromJSON(jsonString) {\r\n if (!jsonString || typeof jsonString !== 'string') {\r\n throw new ValidationError('Valid JSON string is required');\r\n }\r\n\r\n let jsonData;\r\n try {\r\n jsonData = JSON.parse(jsonString);\r\n } catch (error) {\r\n throw new ValidationError('Invalid JSON string format');\r\n }\r\n\r\n return new Green({\r\n _id: jsonData._id,\r\n name: jsonData.name,\r\n location: jsonData.location,\r\n facilities: jsonData.facilities || [],\r\n allowedBookingTypes: jsonData.allowedBookingTypes || [],\r\n rinks: jsonData.rinks || []\r\n });\r\n }\r\n} ","import { v4 as uuidv4 } from 'uuid';\r\nimport { ValidationError } from '../utils/errors.js';\r\nimport { validateBooking } from '../utils/validators.js';\r\nimport { calculateDuration } from '../utils/calculators.js';\r\n\r\n/**\r\n * Represents a booking reservation\r\n */\r\nexport class Booking {\r\n /**\r\n * Create a new Booking\r\n * @param {Object} data - Booking configuration\r\n * @param {string} [data._id] - Unique identifier (auto-generated if not provided)\r\n * @param {string} data.memberId - Member GUID making the booking\r\n * @param {string} data.rinkId - Reference to the booked rink\r\n * @param {string} data.bookingTypeId - Reference to the booking type\r\n * @param {Date} data.date - Booking date\r\n * @param {Date} data.start - Booking start time\r\n * @param {Date} data.end - Booking end time\r\n * @param {string} [data.status='pending'] - Booking status\r\n * @param {Array<string>} [data.participants] - Array of member IDs\r\n * @param {string} [data.notes] - Additional booking notes\r\n * @param {Date} [data.createdDate] - Timestamp of booking creation\r\n * @param {Date} [data.updatedDate] - Timestamp of last update\r\n */\r\n constructor(data) {\r\n if (!data || typeof data !== 'object') {\r\n throw new ValidationError('Booking data is required');\r\n }\r\n\r\n // Validate required booking data\r\n validateBooking(data);\r\n\r\n this._id = data._id || uuidv4();\r\n this.memberId = data.memberId;\r\n this.rinkId = data.rinkId;\r\n this.bookingTypeId = data.bookingTypeId;\r\n this.date = new Date(data.date);\r\n this.start = new Date(data.start);\r\n this.end = new Date(data.end);\r\n this.status = data.status || 'pending';\r\n this.participants = Array.isArray(data.participants) ? [...data.participants] : [];\r\n this.notes = data.notes || '';\r\n this.createdDate = data.createdDate ? new Date(data.createdDate) : new Date();\r\n this.updatedDate = data.updatedDate ? new Date(data.updatedDate) : new Date();\r\n\r\n // Ensure member is in participants list\r\n if (!this.participants.includes(this.memberId)) {\r\n this.participants.unshift(this.memberId);\r\n }\r\n\r\n // Validate date consistency\r\n const bookingDay = this.date.toDateString();\r\n const startDay = this.start.toDateString();\r\n const endDay = this.end.toDateString();\r\n\r\n if (bookingDay !== startDay || bookingDay !== endDay) {\r\n throw new ValidationError(\r\n 'Start and end times must be on the same date as the booking date',\r\n 'dateConsistency',\r\n { date: this.date, start: this.start, end: this.end }\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Update booking status\r\n * @param {string} newStatus - New status to set\r\n * @param {string} memberId - Member performing the update\r\n * @returns {Booking} This instance for chaining\r\n */\r\n updateStatus(newStatus, memberId = null) {\r\n const validStatuses = ['pending', 'confirmed', 'cancelled', 'completed'];\r\n \r\n if (!validStatuses.includes(newStatus)) {\r\n throw new ValidationError('Invalid booking status', 'status', newStatus);\r\n }\r\n\r\n // Validate status transitions\r\n const validTransitions = {\r\n pending: ['confirmed', 'cancelled'],\r\n confirmed: ['cancelled', 'completed'],\r\n cancelled: [], // Cannot transition from cancelled\r\n completed: [] // Cannot transition from completed\r\n };\r\n\r\n if (!validTransitions[this.status].includes(newStatus)) {\r\n throw new ValidationError(\r\n `Invalid status transition from ${this.status} to ${newStatus}`,\r\n 'statusTransition',\r\n { from: this.status, to: newStatus }\r\n );\r\n }\r\n\r\n this.status = newStatus;\r\n this.updatedDate = new Date();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Add participant to booking\r\n * @param {string} memberId - Member ID to add\r\n * @returns {Booking} This instance for chaining\r\n */\r\n addParticipant(memberId) {\r\n if (!memberId || typeof memberId !== 'string') {\r\n throw new ValidationError('Valid member ID is required', 'memberId', memberId);\r\n }\r\n\r\n if (!this.participants.includes(memberId)) {\r\n this.participants.push(memberId);\r\n this.updatedDate = new Date();\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove participant from booking\r\n * @param {string} memberId - Member ID to remove\r\n * @returns {Booking} This instance for chaining\r\n */\r\n removeParticipant(memberId) {\r\n if (!memberId || typeof memberId !== 'string') {\r\n throw new ValidationError('Valid member ID is required', 'memberId', memberId);\r\n }\r\n\r\n // Cannot remove the booking owner\r\n if (memberId === this.memberId) {\r\n throw new ValidationError('Cannot remove booking owner from participants', 'memberId', memberId);\r\n }\r\n\r\n const index = this.participants.indexOf(memberId);\r\n if (index > -1) {\r\n this.participants.splice(index, 1);\r\n this.updatedDate = new Date();\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get booking duration in minutes\r\n * @returns {number} Duration in minutes\r\n */\r\n getDuration() {\r\n return calculateDuration(this.start, this.end);\r\n }\r\n\r\n /**\r\n * Check if booking is currently active\r\n * @returns {boolean} True if booking is currently active\r\n */\r\n isActive() {\r\n if (this.status !== 'confirmed') {\r\n return false;\r\n }\r\n\r\n const now = new Date();\r\n return now >= this.start && now <= this.end;\r\n }\r\n\r\n /**\r\n * Check if booking is in the past\r\n * @returns {boolean} True if booking is in the past\r\n */\r\n isPast() {\r\n const now = new Date();\r\n return this.end < now;\r\n }\r\n\r\n /**\r\n * Check if booking is in the future\r\n * @returns {boolean} True if booking is in the future\r\n */\r\n isFuture() {\r\n const now = new Date();\r\n return this.start > now;\r\n }\r\n\r\n /**\r\n * Check if booking can be modified\r\n * @returns {boolean} True if booking can be modified\r\n */\r\n canModify() {\r\n return this.status === 'pending' || this.status === 'confirmed';\r\n }\r\n\r\n /**\r\n * Check if booking can be cancelled\r\n * @returns {boolean} True if booking can be cancelled\r\n */\r\n canCancel() {\r\n return (this.status === 'pending' || this.status === 'confirmed') && !this.isPast();\r\n }\r\n\r\n /**\r\n * Update booking details\r\n * @param {Object} updates - Properties to update\r\n * @param {string} memberId - Member performing the update\r\n * @returns {Booking} This instance for chaining\r\n */\r\n update(updates, memberId = null) {\r\n if (!updates || typeof updates !== 'object') {\r\n throw new ValidationError('Updates object is required');\r\n }\r\n\r\n if (!this.canModify()) {\r\n throw new ValidationError(`Cannot modify booking with status: ${this.status}`, 'status', this.status);\r\n }\r\n\r\n const allowedFields = ['date', 'start', 'end', 'bookingTypeId', 'participants', 'notes'];\r\n const updateKeys = Object.keys(updates);\r\n\r\n for (const key of updateKeys) {\r\n if (!allowedFields.includes(key)) {\r\n throw new ValidationError(`Field '${key}' cannot be updated`, key, updates[key]);\r\n }\r\n }\r\n\r\n // Validate time updates\r\n const newDate = updates.date ? new Date(updates.date) : this.date;\r\n const newStart = updates.start ? new Date(updates.start) : this.start;\r\n const newEnd = updates.end ? new Date(updates.end) : this.end;\r\n\r\n if (newStart >= newEnd) {\r\n throw new ValidationError('End time must be after start time', 'time', {\r\n start: newStart,\r\n end: newEnd\r\n });\r\n }\r\n\r\n // Apply updates\r\n if (updates.date !== undefined) {\r\n this.date = newDate;\r\n }\r\n if (updates.start !== undefined) {\r\n this.start = newStart;\r\n }\r\n if (updates.end !== undefined) {\r\n this.end = newEnd;\r\n }\r\n if (updates.bookingTypeId !== undefined) {\r\n this.bookingTypeId = updates.bookingTypeId;\r\n }\r\n if (updates.participants !== undefined) {\r\n if (!Array.isArray(updates.participants)) {\r\n throw new ValidationError('Participants must be an array', 'participants', updates.participants);\r\n }\r\n this.participants = [...updates.participants];\r\n // Ensure booking owner is in participants\r\n if (!this.participants.includes(this.memberId)) {\r\n this.participants.unshift(this.memberId);\r\n }\r\n }\r\n if (updates.notes !== undefined) {\r\n this.notes = updates.notes || '';\r\n }\r\n\r\n this.updatedDate = new Date();\r\n return this;\r\n }\r\n\r\n /**\r\n * Get booking type reference (requires BookingType instances to be provided)\r\n * @param {Array<BookingType>} bookingTypes - Available booking types\r\n * @returns {BookingType|null} BookingType instance or null if not found\r\n */\r\n getBookingType(bookingTypes) {\r\n if (!Array.isArray(bookingTypes)) {\r\n return null;\r\n }\r\n\r\n return bookingTypes.find(type => type._id === this.bookingTypeId) || null;\r\n }\r\n\r\n /**\r\n * Check if booking overlaps with another booking\r\n * @param {Booking} otherBooking - Other booking to check\r\n * @returns {boolean} True if bookings overlap\r\n */\r\n overlapsWith(otherBooking) {\r\n if (!otherBooking || !(otherBooking instanceof Booking)) {\r\n return false;\r\n }\r\n\r\n // Different rinks don't overlap\r\n if (this.rinkId !== otherBooking.rinkId) {\r\n return false;\r\n }\r\n\r\n // Different dates don't overlap\r\n if (this.date.toDateString() !== otherBooking.date.toDateString()) {\r\n return false;\r\n }\r\n\r\n // Check time overlap\r\n return this.start < otherBooking.end && this.end > otherBooking.start;\r\n }\r\n\r\n /**\r\n * Convert to JSON string for persistence\r\n * @returns {string} JSON string representation\r\n */\r\n toJSON() {\r\n const data = {\r\n _id: this._id,\r\n memberId: this.memberId,\r\n rinkId: this.rinkId,\r\n bookingTypeId: this.bookingTypeId,\r\n date: this.date.toISOString().split('T')[0], // Date only\r\n start: this.start.toISOString(),\r\n end: this.end.toISOString(),\r\n status: this.status,\r\n participants: [...this.participants],\r\n notes: this.notes,\r\n createdDate: this.createdDate.toISOString(),\r\n updatedDate: this.updatedDate.toISOString()\r\n };\r\n return JSON.stringify(data);\r\n }\r\n\r\n /**\r\n * Create Booking instance from JSON string\r\n * @param {string} jsonString - JSON string data\r\n * @returns {Booking} New Booking instance\r\n */\r\n static fromJSON(jsonString) {\r\n if (!jsonString || typeof jsonString !== 'string') {\r\n throw new ValidationError('Valid JSON string is required');\r\n }\r\n\r\n let jsonData;\r\n try {\r\n jsonData = JSON.parse(jsonString);\r\n } catch (error) {\r\n throw new ValidationError('Invalid JSON string format');\r\n }\r\n\r\n return new Booking({\r\n _id: jsonData._id,\r\n memberId: jsonData.memberId,\r\n rinkId: jsonData.rinkId,\r\n bookingTypeId: jsonData.bookingTypeId,\r\n date: new Date(jsonData.date),\r\n start: new Date(jsonData.start),\r\n end: new Date(jsonData.end),\r\n status: jsonData.status,\r\n participants: jsonData.participants || [],\r\n notes: jsonData.notes || '',\r\n createdDate: jsonData.createdDate ? new Date(jsonData.createdDate) : new Date(),\r\n updatedDate: jsonData.updatedDate ? new Date(jsonData.updatedDate) : new Date()\r\n });\r\n }\r\n} ","import { v4 as uuidv4 } from 'uuid';\r\nimport { ValidationError } from '../utils/errors.js';\r\nimport { validateColor } from '../utils/validators.js';\r\n\r\n/**\r\n * Represents a type of booking with visual styling and restrictions\r\n */\r\nexport class BookingType {\r\n /**\r\n * Create a new BookingType\r\n * @param {Object} data - BookingType configuration\r\n * @param {string} [data._id] - Unique identifier (auto-generated if not provided)\r\n * @param {string} data.name - Display name for the booking type\r\n * @param {string} data.backgroundColor - Background color (hex color)\r\n * @param {string} data.foregroundColor - Text/foreground color (hex color)\r\n * @param {string} [data.description] - Optional description\r\n * @param {number} [data.priority=1] - Numeric priority for conflicts (higher = more important)\r\n */\r\n constructor(data) {\r\n if (!data || typeof data !== 'object') {\r\n throw new ValidationError('BookingType data is required');\r\n }\r\n\r\n if (!data.name || typeof data.name !== 'string') {\r\n throw new ValidationError('Name is required', 'name', data.name);\r\n }\r\n\r\n if (!data.backgroundColor) {\r\n throw new ValidationError('Background color is required', 'backgroundColor', data.backgroundColor);\r\n }\r\n\r\n if (!data.foregroundColor) {\r\n throw new ValidationError('Foreground color is required', 'foregroundColor', data.foregroundColor);\r\n }\r\n\r\n // Validate colors\r\n validateColor(data.backgroundColor, 'backgroundColor');\r\n validateColor(data.foregroundColor, 'foregroundColor');\r\n\r\n this._id = data._id || uuidv4();\r\n this.name = data.name.trim();\r\n this.backgroundColor = data.backgroundColor;\r\n this.foregroundColor = data.foregroundColor;\r\n this.description = data.description ? data.description.trim() : '';\r\n this.priority = typeof data.priority === 'number' ? data.priority : 1;\r\n\r\n // Validate priority range\r\n if (this.priority < 1 || this.priority > 100) {\r\n throw new ValidationError('Priority must be between 1 and 100', 'priority', this.priority);\r\n }\r\n }\r\n\r\n /**\r\n * Update booking type properties\r\n * @param {Object} updates - Properties to update\r\n * @returns {BookingType} This instance for chaining\r\n */\r\n update(updates) {\r\n if (!updates || typeof updates !== 'object') {\r\n throw new ValidationError('Updates object is required');\r\n }\r\n\r\n // Validate update