/** * Timeslot Creator - Standalone JavaScript Library v2.0 * Supports multiple date ranges, each with custom settings * All UI elements can be shown/hidden via configuration * * Usage: * *
* */ (function(global) { 'use strict'; const STORAGE_KEY = 'timeslot_creator_config'; const DAY_NAMES = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const HOLIDAYS = { US: [ {name: "New Year's Day", date: "01-01"}, {name: "Martin Luther King Jr. Day", date: "third-monday-january"}, {name: "Presidents' Day", date: "third-monday-february"}, {name: "Memorial Day", date: "last-monday-may"}, {name: "Independence Day", date: "07-04"}, {name: "Labor Day", date: "first-monday-september"}, {name: "Columbus Day", date: "second-monday-october"}, {name: "Veterans Day", date: "11-11"}, {name: "Thanksgiving", date: "fourth-thursday-november"}, {name: "Christmas Day", date: "12-25"} ], UK: [ {name: "New Year's Day", date: "01-01"}, {name: "Good Friday", date: "easter-2"}, {name: "Easter Monday", date: "easter+1"}, {name: "Early May Bank Holiday", date: "first-monday-may"}, {name: "Spring Bank Holiday", date: "last-monday-may"}, {name: "Summer Bank Holiday", date: "last-monday-august"}, {name: "Christmas Day", date: "12-25"}, {name: "Boxing Day", date: "12-26"} ], CA: [ {name: "New Year's Day", date: "01-01"}, {name: "Good Friday", date: "easter-2"}, {name: "Victoria Day", date: "monday-before-may-25"}, {name: "Canada Day", date: "07-01"}, {name: "Labour Day", date: "first-monday-september"}, {name: "Thanksgiving", date: "second-monday-october"}, {name: "Christmas Day", date: "12-25"}, {name: "Boxing Day", date: "12-26"} ] }; const TIMEZONES = [ 'UTC', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'America/Toronto', 'America/Vancouver', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Rome', 'Europe/Madrid', 'Europe/Amsterdam', 'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Singapore', 'Asia/Dubai', 'Asia/Kolkata', 'Australia/Sydney', 'Australia/Melbourne', 'Pacific/Auckland', 'Pacific/Honolulu' ]; const CSS = ` .tsc-container { font-family: system-ui, -apple-system, sans-serif; background: #f3f4f6; padding: 2rem; } .tsc-container * { box-sizing: border-box; } .tsc-header { margin-bottom: 2rem; } .tsc-header h1 { font-size: 1.875rem; font-weight: bold; color: #1f2937; margin: 0 0 0.5rem; } .tsc-header p { color: #6b7280; margin: 0 0 1rem; } .tsc-header-buttons { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; } .tsc-btn { padding: 0.5rem 1rem; border-radius: 0.5rem; border: none; cursor: pointer; font-size: 0.875rem; transition: background 0.2s; } .tsc-btn-primary { background: #3b82f6; color: white; } .tsc-btn-primary:hover { background: #2563eb; } .tsc-btn-purple { background: #8b5cf6; color: white; } .tsc-btn-purple:hover { background: #7c3aed; } .tsc-btn-gray { background: #6b7280; color: white; } .tsc-btn-gray:hover { background: #4b5563; } .tsc-btn-green { background: #10b981; color: white; } .tsc-btn-green:hover { background: #059669; } .tsc-btn-dark { background: #374151; color: white; } .tsc-btn-dark:hover { background: #1f2937; } .tsc-btn-red { background: #ef4444; color: white; } .tsc-btn-red:hover { background: #dc2626; } .tsc-btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; } .tsc-status { font-size: 0.875rem; color: #6b7280; } .tsc-tabs { display: flex; flex-wrap: wrap; gap: 0.5rem; border-bottom: 1px solid #e5e7eb; margin-bottom: 1.5rem; } .tsc-tab { padding: 0.5rem 1rem; background: none; border: none; cursor: pointer; font-weight: 500; color: #6b7280; border-bottom: 2px solid transparent; } .tsc-tab.active { color: #3b82f6; border-bottom-color: #3b82f6; } .tsc-tab:hover { color: #3b82f6; } .tsc-panel { display: none; background: white; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); padding: 1.5rem; } .tsc-panel.active { display: block; } .tsc-panel h2 { font-size: 1.25rem; font-weight: 600; margin: 0 0 1rem; } .tsc-panel p { color: #6b7280; margin: 0 0 1rem; } .tsc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; } .tsc-form-group { margin-bottom: 1rem; } .tsc-label { display: block; font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.5rem; } .tsc-input, .tsc-select { width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.5rem; font-size: 1rem; } .tsc-input:focus, .tsc-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); } .tsc-checkbox-group { display: flex; align-items: center; margin-bottom: 0.75rem; } .tsc-checkbox { width: 1rem; height: 1rem; margin-right: 0.5rem; } .tsc-rule { border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; } .tsc-day-btns { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; } .tsc-day-btn { padding: 0.25rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.25rem; background: white; cursor: pointer; } .tsc-day-btn.selected { background: #3b82f6; color: white; border-color: #3b82f6; } .tsc-time-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } .tsc-time-input { width: 120px; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.5rem; } .tsc-remove-btn { color: #ef4444; background: none; border: none; cursor: pointer; margin-left: auto; } .tsc-add-link { color: #3b82f6; background: none; border: none; cursor: pointer; padding: 0; margin-top: 0.5rem; } .tsc-meeting-type { border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; } .tsc-meeting-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1rem; align-items: end; } .tsc-color-input { width: 100%; height: 2.5rem; border: 1px solid #d1d5db; border-radius: 0.5rem; cursor: pointer; } .tsc-exception { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: #f9fafb; border-radius: 0.5rem; margin-bottom: 0.5rem; } .tsc-exception-info span { margin-right: 0.5rem; } .tsc-blocked { color: #dc2626; } .tsc-limited { color: #d97706; } .tsc-calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } .tsc-calendar-nav { display: flex; gap: 0.5rem; align-items: center; } .tsc-nav-btn { padding: 0.25rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.25rem; background: white; cursor: pointer; } .tsc-legend { display: flex; gap: 1rem; margin-bottom: 1rem; font-size: 0.875rem; flex-wrap: wrap; } .tsc-legend-item { display: flex; align-items: center; } .tsc-legend-color { width: 1rem; height: 1rem; border-radius: 0.25rem; margin-right: 0.25rem; } .tsc-calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: #e5e7eb; } .tsc-calendar-header-cell { background: #f9fafb; padding: 0.5rem; text-align: center; font-weight: 500; } .tsc-calendar-day { background: white; padding: 0.5rem; min-height: 100px; } .tsc-calendar-day.weekend { background: #f3f4f6; } .tsc-calendar-day.blocked { background: #fee2e2; } .tsc-calendar-day.available { background: #dcfce7; } .tsc-calendar-day.exception { background: #fef3c7; } .tsc-calendar-day-num { font-weight: 500; margin-bottom: 0.25rem; } .tsc-calendar-day-num.other-month { color: #9ca3af; } .tsc-calendar-info { font-size: 0.75rem; } .tsc-calendar-slots { font-size: 0.65rem; padding: 2px 4px; margin: 1px 0; border-radius: 3px; background: #dbeafe; color: #1e40af; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tsc-list-controls { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: end; } .tsc-list-day { border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; } .tsc-list-day.blocked { background: #fee2e2; } .tsc-list-day.available { background: #dcfce7; } .tsc-list-day h3 { margin: 0 0 0.5rem; font-weight: 500; } .tsc-list-day p { margin: 0 0 0.5rem; font-size: 0.875rem; } .tsc-slots-wrap { display: flex; flex-wrap: wrap; } .tsc-slot { display: inline-block; padding: 0.25rem 0.5rem; margin: 0.25rem; font-size: 0.875rem; border-radius: 0.25rem; background: #e0e7ff; } .tsc-export-section { margin-bottom: 1.5rem; } .tsc-export-section h3 { font-weight: 500; margin: 0 0 0.5rem; } .tsc-json-preview { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow: auto; max-height: 400px; font-family: monospace; font-size: 0.75rem; white-space: pre-wrap; word-break: break-all; } .tsc-hidden { display: none !important; } .tsc-range-card { border: 2px solid #e5e7eb; border-radius: 0.75rem; padding: 1rem; margin-bottom: 1rem; transition: border-color 0.2s; } .tsc-range-card.selected { border-color: #3b82f6; background: #eff6ff; } .tsc-range-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } .tsc-range-card-title { font-weight: 600; font-size: 1rem; margin: 0; } .tsc-range-card-dates { color: #6b7280; font-size: 0.875rem; } .tsc-range-card-stats { display: flex; gap: 1rem; font-size: 0.75rem; color: #6b7280; margin-top: 0.5rem; } .tsc-range-actions { display: flex; gap: 0.5rem; } .tsc-range-selector { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem; padding: 0.75rem; background: #f9fafb; border-radius: 0.5rem; } .tsc-range-selector label { font-weight: 500; } .tsc-global-settings { background: #f0fdf4; border: 1px solid #86efac; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; } .tsc-global-settings h3 { margin: 0 0 0.75rem; font-size: 1rem; color: #166534; } `; function injectStyles() { if (document.getElementById('tsc-styles')) return; const style = document.createElement('style'); style.id = 'tsc-styles'; style.textContent = CSS; document.head.appendChild(style); } function generateId() { return 'range_' + Math.random().toString(36).substr(2, 9); } function formatDate(d) { return d.toISOString().split('T')[0]; } function convertFromUtc(utcIsoString, toTimezone) { try { const utcDate = new Date(utcIsoString); const options = { timeZone: toTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; const parts = new Intl.DateTimeFormat('en-CA', options).formatToParts(utcDate); const get = (type) => parts.find(p => p.type === type)?.value || '00'; const year = get('year'); const month = get('month'); const day = get('day'); let hour = get('hour'); if (hour === '24') hour = '00'; const minute = get('minute'); const second = get('second'); return { date: `${year}-${month}-${day}`, time: `${hour}:${minute}`, datetime: `${year}-${month}-${day} ${hour}:${minute}:${second}` }; } catch (e) { console.error('Timezone conversion error:', e); return { date: utcIsoString.split('T')[0], time: '00:00', datetime: utcIsoString.replace('T', ' ').replace('Z', '') }; } } function getDefaultRange() { const today = new Date(); const future = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); return { id: generateId(), name: 'Default Schedule', date_range_start: formatDate(today), date_range_end: formatDate(future), block_weekends: true, block_holidays: true, availability_rules: [ { days: [1, 2, 3, 4, 5], start_time: '09:00', end_time: '17:00' } ], meeting_types: [ { id: 1, name: '15 Minute Meeting', duration: 15, buffer: 5, color: '#3b82f6', active: true }, { id: 2, name: '30 Minute Meeting', duration: 30, buffer: 10, color: '#10b981', active: true }, { id: 3, name: '45 Minute Meeting', duration: 45, buffer: 15, color: '#8b5cf6', active: true } ], exceptions: [] }; } function getDefaultConfig() { const defaultRange = getDefaultRange(); return { timezone: 'America/New_York', holiday_country: 'US', ranges: [defaultRange], active_range_id: defaultRange.id }; } function loadConfig(storageKey) { try { const saved = localStorage.getItem(storageKey); if (saved) { const config = JSON.parse(saved); if (!config.ranges || !Array.isArray(config.ranges)) { const range = getDefaultRange(); range.date_range_start = config.date_range_start || range.date_range_start; range.date_range_end = config.date_range_end || range.date_range_end; range.block_weekends = config.block_weekends !== undefined ? config.block_weekends : true; range.block_holidays = config.block_holidays !== undefined ? config.block_holidays : true; range.availability_rules = config.availability_rules || range.availability_rules; range.meeting_types = config.meeting_types || range.meeting_types; range.exceptions = config.exceptions || []; return { timezone: config.timezone || 'America/New_York', holiday_country: config.holiday_country || 'US', ranges: [range], active_range_id: range.id }; } if (!config.ranges.length) { const defaultRange = getDefaultRange(); config.ranges = [defaultRange]; config.active_range_id = defaultRange.id; } if (!config.active_range_id || !config.ranges.find(r => r.id === config.active_range_id)) { config.active_range_id = config.ranges[0].id; } return config; } } catch (e) {} return getDefaultConfig(); } function saveConfig(config, storageKey, callback) { try { localStorage.setItem(storageKey, JSON.stringify(config)); if (callback) callback(config); return true; } catch (e) { return false; } } class SlotGenerator { constructor(globalConfig, rangeConfig) { this.timezone = globalConfig.timezone || 'UTC'; this.holidayCountry = globalConfig.holiday_country || 'US'; this.holidays = HOLIDAYS[this.holidayCountry] || []; this.availabilityRules = rangeConfig.availability_rules || []; this.exceptions = rangeConfig.exceptions || []; this.meetingTypes = rangeConfig.meeting_types || []; this.blockWeekends = rangeConfig.block_weekends !== false; this.blockHolidays = rangeConfig.block_holidays !== false; this.rangeStart = rangeConfig.date_range_start || null; this.rangeEnd = rangeConfig.date_range_end || null; } toUTC(dateStr, timeStr) { try { const [year, month, day] = dateStr.split('-').map(Number); const [hour, minute] = timeStr.split(':').map(Number); const utcEstimate = Date.UTC(year, month - 1, day, hour, minute, 0, 0); const formatter = new Intl.DateTimeFormat('en-US', { timeZone: this.timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }); const parts = formatter.formatToParts(new Date(utcEstimate)); const getPart = (type) => { const p = parts.find(x => x.type === type); return p ? parseInt(p.value) : 0; }; const tzMonth = getPart('month'); const tzDay = getPart('day'); let tzHour = getPart('hour'); if (tzHour === 24) tzHour = 0; const tzMinute = getPart('minute'); const targetMinutes = day * 24 * 60 + hour * 60 + minute; const formattedMinutes = tzDay * 24 * 60 + tzHour * 60 + tzMinute; const offsetMinutes = (formattedMinutes - targetMinutes) * 60 * 1000; let correction = 0; if (tzMonth !== month) { correction = (tzMonth > month || (tzMonth === 1 && month === 12)) ? -24 * 60 * 60 * 1000 : 24 * 60 * 60 * 1000; } const correctedUtc = utcEstimate - offsetMinutes + correction; return new Date(correctedUtc).toISOString(); } catch (e) { const [year, month, day] = dateStr.split('-').map(Number); const [hour, minute] = timeStr.split(':').map(Number); return new Date(Date.UTC(year, month - 1, day, hour, minute)).toISOString(); } } generateSlots(startDate, endDate) { const slots = {}; const current = new Date(startDate + 'T00:00:00'); const end = new Date(endDate + 'T00:00:00'); while (current <= end) { const dateStr = formatDate(current); const dayOfWeek = current.getDay() === 0 ? 7 : current.getDay(); const daySlots = this.generateDaySlots(current, dateStr, dayOfWeek); if (daySlots) slots[dateStr] = daySlots; current.setDate(current.getDate() + 1); } return slots; } generateDaySlots(date, dateStr, dayOfWeek) { if (this.rangeStart && dateStr < this.rangeStart) { return { blocked: true, reason: 'out_of_range', slots: [] }; } if (this.rangeEnd && dateStr > this.rangeEnd) { return { blocked: true, reason: 'out_of_range', slots: [] }; } if (this.blockWeekends && (dayOfWeek === 6 || dayOfWeek === 7)) { return { blocked: true, reason: 'weekend', slots: [] }; } const holiday = this.getHoliday(date); if (this.blockHolidays && holiday) { return { blocked: true, reason: 'holiday', holiday_name: holiday.name, slots: [] }; } if (this.isBlockedException(dateStr)) { return { blocked: true, reason: 'exception', slots: [] }; } const dayRules = this.getDayRules(dateStr, dayOfWeek); if (!dayRules.length) { return { blocked: true, reason: 'no_availability', slots: [] }; } const availableSlots = []; for (const mt of this.meetingTypes) { if (mt.active === false) continue; const typeSlots = this.generateMeetingSlots(date, dateStr, dayRules, mt); availableSlots.push(...typeSlots); } availableSlots.sort((a, b) => a.start_time.localeCompare(b.start_time)); return { blocked: false, slots: availableSlots }; } generateMeetingSlots(date, dateStr, dayRules, meetingType) { const slots = []; const duration = meetingType.duration; const buffer = meetingType.buffer || 0; const increment = duration + buffer; for (const rule of dayRules) { const startTime = (rule.start_time || '09:00').substring(0, 5); const endTime = (rule.end_time || '17:00').substring(0, 5); let [startH, startM] = startTime.split(':').map(Number); const [endH, endM] = endTime.split(':').map(Number); const endMinutes = endH * 60 + endM; while (true) { const slotStartMin = startH * 60 + startM; const slotEndMin = slotStartMin + duration; if (slotEndMin > endMinutes) break; const slotStartTime = String(startH).padStart(2, '0') + ':' + String(startM).padStart(2, '0'); const slotEndH = Math.floor(slotEndMin / 60); const slotEndM = slotEndMin % 60; const slotEndTime = String(slotEndH).padStart(2, '0') + ':' + String(slotEndM).padStart(2, '0'); slots.push({ id: 'slot_' + Math.random().toString(36).substr(2, 9), date: dateStr, start_time: slotStartTime, end_time: slotEndTime, start_utc: this.toUTC(dateStr, slotStartTime), end_utc: this.toUTC(dateStr, slotEndTime), duration: duration, meeting_type: meetingType.name, meeting_type_id: meetingType.id, buffer_after: buffer, color: meetingType.color, timezone: this.timezone }); startM += increment; while (startM >= 60) { startH++; startM -= 60; } } } return slots; } getDayRules(dateStr, dayOfWeek) { const exRules = this.getExceptionRules(dateStr); if (exRules.length) return exRules; const rules = []; for (const rule of this.availabilityRules) { if (rule.days && rule.days.includes(dayOfWeek)) { rules.push({ start_time: rule.start_time, end_time: rule.end_time }); } } return rules; } isBlockedException(dateStr) { return this.exceptions.some(ex => ex.date === dateStr && (ex.type || 'blocked') === 'blocked'); } getExceptionRules(dateStr) { for (const ex of this.exceptions) { if (ex.date === dateStr && ex.type === 'limited') { return [{ start_time: ex.start_time, end_time: ex.end_time }]; } } return []; } getHoliday(date) { const year = date.getFullYear(); const monthDay = String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0'); for (const h of this.holidays) { const hDate = this.resolveHolidayDate(h.date, year); if (hDate === monthDay) return h; } return null; } resolveHolidayDate(pattern, year) { if (/^\d{2}-\d{2}$/.test(pattern)) return pattern; if (pattern.includes('easter')) return this.resolveEasterDate(pattern, year); if (pattern.includes('monday-before')) return this.resolveMondayBefore(pattern, year); const ordinals = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5, last: 'last' }; const weekdays = { sunday: 0, monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6 }; const months = { january: 0, february: 1, march: 2, april: 3, may: 4, june: 5, july: 6, august: 7, september: 8, october: 9, november: 10, december: 11 }; const parts = pattern.split('-'); if (parts.length >= 3) { const ord = ordinals[parts[0]]; const wd = weekdays[parts[1]]; const mo = months[parts[2]]; if (ord !== undefined && wd !== undefined && mo !== undefined) { if (ord === 'last') return this.getLastWeekdayOfMonth(year, mo, wd); return this.getNthWeekdayOfMonth(year, mo, wd, ord); } } return null; } resolveEasterDate(pattern, year) { const easter = this.calculateEaster(year); const match = pattern.match(/easter([+-])(\d+)/); if (match) { const days = parseInt(match[2]) * (match[1] === '-' ? -1 : 1); easter.setDate(easter.getDate() + days); } return String(easter.getMonth() + 1).padStart(2, '0') + '-' + String(easter.getDate()).padStart(2, '0'); } calculateEaster(year) { const a = year % 19; const b = Math.floor(year / 100); const c = year % 100; const d = Math.floor(b / 4); const e = b % 4; const f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3); const h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4); const k = c % 4; const l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31) - 1; const day = ((h + l - 7 * m + 114) % 31) + 1; return new Date(year, month, day); } resolveMondayBefore(pattern, year) { const match = pattern.match(/monday-before-(\w+)-(\d+)/); if (!match) return null; const months = { january: 0, february: 1, march: 2, april: 3, may: 4, june: 5, july: 6, august: 7, september: 8, october: 9, november: 10, december: 11 }; const mo = months[match[1]]; const day = parseInt(match[2]); const d = new Date(year, mo, day); while (d.getDay() !== 1) d.setDate(d.getDate() - 1); return String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); } getNthWeekdayOfMonth(year, month, weekday, n) { const first = new Date(year, month, 1); const firstWd = first.getDay(); let daysUntil = (weekday - firstWd + 7) % 7; const day = 1 + daysUntil + ((n - 1) * 7); const d = new Date(year, month, day); if (d.getMonth() !== month) return null; return String(month + 1).padStart(2, '0') + '-' + String(day).padStart(2, '0'); } getLastWeekdayOfMonth(year, month, weekday) { const last = new Date(year, month + 1, 0); while (last.getDay() !== weekday) last.setDate(last.getDate() - 1); return String(month + 1).padStart(2, '0') + '-' + String(last.getDate()).padStart(2, '0'); } } function createTimeslotCreator(container, options) { const opts = options || {}; const storageKey = opts.storageKey || STORAGE_KEY; let config = loadConfig(storageKey); let currentMonth = new Date(); let generatedSlots = null; const uiOptions = { showTimezone: opts.showTimezone !== false, showHolidayCountry: opts.showHolidayCountry !== false, showBlockWeekends: opts.showBlockWeekends !== false, showBlockHolidays: opts.showBlockHolidays !== false, showAvailability: opts.showAvailability !== false, showMeetingTypes: opts.showMeetingTypes !== false, showExceptions: opts.showExceptions !== false, showCalendar: opts.showCalendar !== false, showListView: opts.showListView !== false, showExport: opts.showExport !== false, showRangeSelector: opts.showRangeSelector !== false, showDateRange: opts.showDateRange !== false, showRangeName: opts.showRangeName !== false, showGlobalSettings: opts.showGlobalSettings !== false, showHeaderButtons: opts.showHeaderButtons !== false }; const onSave = opts.onSave || function() {}; const onSlotsGenerated = opts.onSlotsGenerated || function() {}; const onManualSave = opts.onManualSave || null; function getActiveRange() { if (!config.ranges.length) { const newRange = getDefaultRange(); config.ranges.push(newRange); config.active_range_id = newRange.id; } let range = config.ranges.find(r => r.id === config.active_range_id); if (!range) { range = config.ranges[0]; config.active_range_id = range.id; } return range; } function setActiveRange(rangeId) { config.active_range_id = rangeId; save(); render(); } function addRange() { const newRange = getDefaultRange(); newRange.name = 'Schedule ' + (config.ranges.length + 1); config.ranges.push(newRange); config.active_range_id = newRange.id; save(); render(); } function deleteRange(rangeId) { if (config.ranges.length <= 1) { alert('Cannot delete the last schedule. You must have at least one.'); return; } config.ranges = config.ranges.filter(r => r.id !== rangeId); if (config.active_range_id === rangeId) { config.active_range_id = config.ranges[0].id; } save(); render(); } function duplicateRange(rangeId) { const source = config.ranges.find(r => r.id === rangeId); if (!source) return; const newRange = JSON.parse(JSON.stringify(source)); newRange.id = generateId(); newRange.name = source.name + ' (Copy)'; config.ranges.push(newRange); config.active_range_id = newRange.id; save(); render(); } function save() { saveConfig(config, storageKey, onSave); updateStatus('Saved!'); } function updateStatus(msg) { const el = container.querySelector('.tsc-status'); if (el) { el.textContent = msg; setTimeout(() => el.textContent = '', 2000); } } function generateSlots(startDate, endDate, rangeId) { const range = rangeId ? config.ranges.find(r => r.id === rangeId) : getActiveRange(); const gen = new SlotGenerator(config, range); const slots = gen.generateSlots(startDate, endDate); const result = { generated_at: new Date().toISOString(), timezone: config.timezone, range_id: range.id, range_name: range.name, date_range: { start: startDate, end: endDate }, settings: { block_weekends: range.block_weekends, block_holidays: range.block_holidays, holiday_country: config.holiday_country }, meeting_types: range.meeting_types, slots: slots, summary: { total_days: Object.keys(slots).length, available_days: Object.values(slots).filter(s => !s.blocked).length, blocked_days: Object.values(slots).filter(s => s.blocked).length, total_slots: Object.values(slots).reduce((sum, s) => sum + (s.slots ? s.slots.length : 0), 0) } }; generatedSlots = result; onSlotsGenerated(result); return result; } function generateAllRangesSlots() { const allResults = []; for (const range of config.ranges) { const result = generateSlots(range.date_range_start, range.date_range_end, range.id); allResults.push(result); } return { generated_at: new Date().toISOString(), timezone: config.timezone, holiday_country: config.holiday_country, ranges: allResults }; } function exportSlots(options = {}) { const { rangeId = null, convertToTimezone = 'America/New_York', metadata = {} } = options; const range = rangeId ? config.ranges.find(r => r.id === rangeId) : getActiveRange(); if (!range) return null; const gen = new SlotGenerator(config, range); const slotsData = gen.generateSlots(range.date_range_start, range.date_range_end); const originalTimezone = config.timezone; const flatSlots = []; for (const [dateStr, dayData] of Object.entries(slotsData)) { if (dayData.blocked || !dayData.slots || dayData.slots.length === 0) { continue; } for (const slot of dayData.slots) { const convertedStart = convertFromUtc(slot.start_utc, convertToTimezone); const convertedEnd = convertFromUtc(slot.end_utc, convertToTimezone); const originalStart = convertFromUtc(slot.start_utc, originalTimezone); const originalEnd = convertFromUtc(slot.end_utc, originalTimezone); flatSlots.push({ id: slot.id, original_start: originalStart.datetime, original_end: originalEnd.datetime, original_timezone: originalTimezone, converted_start: convertedStart.datetime, converted_end: convertedEnd.datetime, converted_timezone: convertToTimezone, start_utc: slot.start_utc.replace('T', ' ').replace('.000Z', ''), end_utc: slot.end_utc.replace('T', ' ').replace('.000Z', ''), duration: slot.duration, meeting_type: slot.meeting_type, meeting_type_id: slot.meeting_type_id, buffer_after: slot.buffer_after, color: slot.color }); } } flatSlots.sort((a, b) => { return a.original_start.localeCompare(b.original_start); }); return { generated_at: new Date().toISOString(), range_id: range.id, range_name: range.name, date_range: { start: range.date_range_start, end: range.date_range_end }, original_timezone: originalTimezone, converted_timezone: convertToTimezone, total_slots: flatSlots.length, ...metadata, slots: flatSlots }; } function exportAllSlots(options = {}) { const { convertToTimezone = 'America/New_York', metadata = {} } = options; const originalTimezone = config.timezone; const allSlots = []; const rangesInfo = []; for (const range of config.ranges) { rangesInfo.push({ id: range.id, name: range.name, date_range_start: range.date_range_start, date_range_end: range.date_range_end, block_weekends: range.block_weekends, block_holidays: range.block_holidays, availability_rules: range.availability_rules, meeting_types: range.meeting_types, exceptions: range.exceptions }); const gen = new SlotGenerator(config, range); const slotsData = gen.generateSlots(range.date_range_start, range.date_range_end); for (const [dateStr, dayData] of Object.entries(slotsData)) { if (dayData.blocked || !dayData.slots || dayData.slots.length === 0) { continue; } for (const slot of dayData.slots) { const convertedStart = convertFromUtc(slot.start_utc, convertToTimezone); const convertedEnd = convertFromUtc(slot.end_utc, convertToTimezone); const originalStart = convertFromUtc(slot.start_utc, originalTimezone); const originalEnd = convertFromUtc(slot.end_utc, originalTimezone); allSlots.push({ id: slot.id, range_id: range.id, range_name: range.name, original_start: originalStart.datetime, original_end: originalEnd.datetime, original_timezone: originalTimezone, converted_start: convertedStart.datetime, converted_end: convertedEnd.datetime, converted_timezone: convertToTimezone, start_utc: slot.start_utc.replace('T', ' ').replace('.000Z', ''), end_utc: slot.end_utc.replace('T', ' ').replace('.000Z', ''), duration: slot.duration, meeting_type: slot.meeting_type, meeting_type_id: slot.meeting_type_id, buffer_after: slot.buffer_after, color: slot.color }); } } } allSlots.sort((a, b) => a.original_start.localeCompare(b.original_start)); return { generated_at: new Date().toISOString(), original_timezone: originalTimezone, converted_timezone: convertToTimezone, total_ranges: rangesInfo.length, total_slots: allSlots.length, ...metadata, ranges: rangesInfo, slots: allSlots }; } function render() { const range = getActiveRange(); const tabs = []; if (uiOptions.showRangeSelector) tabs.push({ id: 'ranges', label: 'Schedules' }); if (uiOptions.showGlobalSettings) tabs.push({ id: 'settings', label: 'Settings' }); if (uiOptions.showAvailability) tabs.push({ id: 'availability', label: 'Availability' }); if (uiOptions.showMeetingTypes) tabs.push({ id: 'meeting-types', label: 'Meeting Types' }); if (uiOptions.showExceptions) tabs.push({ id: 'exceptions', label: 'Exceptions' }); if (uiOptions.showCalendar) tabs.push({ id: 'calendar', label: 'Calendar View' }); if (uiOptions.showListView) tabs.push({ id: 'list', label: 'List View' }); if (uiOptions.showExport) tabs.push({ id: 'export', label: 'Export' }); const activeTab = tabs.length > 0 ? tabs[0].id : 'settings'; container.innerHTML = `Configure your meeting availability and generate scheduling slots
${uiOptions.showHeaderButtons ? ` ` : ''}Create multiple schedules with different date ranges and availability settings.
These settings apply to all schedules.
Set your regular working hours for each day of the week.
Define different types of meetings with their durations and buffer times.
Add specific dates that are blocked or have limited availability.
No exceptions added yet.
'}${reason}
${dayData.slots.length} available slots