Files
Sitzplatzbuchungstool/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json
T
2026-08-20 08:24:24 +00:00

96 lines
46 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"backgroundColor": "#FFFFFF",
"bookingClicked": "{{\n storeValue(\n \"selectedBooking\",\n Custom5.model.selectedBooking,\n false\n )\n .then(() =>\n showModal(\n MyBookingDetailsModal.name\n )\n )\n}}",
"bookingSelected": "{{ BookingSelect.openBookingWidget() }}",
"borderColor": "#E0DEDE",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"borderWidth": "1",
"bottomRow": 69,
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"defaultModel": "{{\n {\n seatId:\n appsmith.store.selectedSeatId || \"\",\n\n seatName:\n appsmith.store.selectedSeatName || \"\",\n\n currentUser:\n appsmith.user.email ||\n appsmith.user.username ||\n \"\",\n\n events:\n GetSeatBookings.data || []\n }\n}}",
"dynamicBindingPathList": [
{
"key": "theme"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
},
{
"key": "defaultModel"
}
],
"dynamicHeight": "FIXED",
"dynamicPropertyPathList": [
{
"key": "seatSelected"
},
{
"key": "bookingSelected"
},
{
"key": "bookingClicked"
},
{
"key": "timeSelected"
}
],
"dynamicTriggerPathList": [
{
"key": "seatSelected"
},
{
"key": "bookingSelected"
},
{
"key": "timeSelected"
},
{
"key": "bookingClicked"
}
],
"events": [
"seatSelected",
"bookingSelected",
"timeSelected",
"bookingClicked"
],
"isCanvas": false,
"isLoading": false,
"isSearchWildcard": true,
"isVisible": true,
"key": "3r58ovsolf",
"leftColumn": 0,
"maxDynamicHeight": 9000,
"minDynamicHeight": 4,
"mobileBottomRow": 40,
"mobileLeftColumn": 3,
"mobileRightColumn": 26,
"mobileTopRow": 10,
"needsErrorInfo": false,
"parentColumnSpace": 7.40625,
"parentId": "ekq4ry46pv",
"parentRowSpace": 10,
"renderMode": "CANVAS",
"rightColumn": 64,
"seatSelected": "{{\n storeValue(\n \"selectedSeat\",\n Custom4.model?.selectedSeat || null\n )\n .then(() =>\n storeValue(\n \"selectedSeatId\",\n Custom4.model?.selectedSeat?.id ||\n Custom4.model?.seatId ||\n null\n )\n )\n .then(() =>\n storeValue(\n \"selectedSeatName\",\n Custom4.model?.selectedSeat?.name ||\n Custom4.model?.seatName ||\n null\n )\n )\n .then(() =>\n showModal(\n SeatCalendar.name\n )\n )\n}}",
"srcDoc": {
"css": "@charset \"UTF-8\";\nhtml,\nbody {\n margin: 0;\n padding: 0;\n width: 100%;\n min-height: 100%;\n font-family: Arial, sans-serif;\n overflow: hidden;\n}\n\n/* Äußerer Custom-Widget-Bereich */\n#calendarWrapper {\n width: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: 10px;\n background: #ffffff;\n overflow: visible;\n}\n\n/* Kopfbereich */\n#calendarHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 10px;\n padding: 8px 10px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n background: #f9fafb;\n}\n\n#calendarSeatTitle {\n font-size: 15px;\n font-weight: 600;\n color: #374151;\n}\n\n/* Kalender */\n#calendar {\n width: 100%;\n min-height: 0;\n box-sizing: border-box;\n --calendar-selection-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n --fc-highlight-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n --fc-today-bg-color: transparent;\n}\n\n/* Auswahltext */\n#calendarSelection {\n margin-top: 10px;\n padding: 8px 10px;\n border: 1px solid #d1d5db;\n border-radius: 5px;\n background: #f9fafb;\n color: #374151;\n font-size: 13px;\n}\n\n/* FullCalendar-Grundformatierung */\n#calendar .fc {\n width: 100%;\n font-size: 13px;\n --fc-highlight-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n --fc-today-bg-color: transparent;\n}\n\n#calendar .fc .fc-button {\n background: #374151;\n border-color: #374151;\n}\n\n#calendar .fc .fc-button:hover,\n#calendar .fc .fc-button-active {\n background: #111827;\n border-color: #111827;\n}\n\n#calendar .fc .fc-event {\n cursor: pointer;\n}\n\n/*\n * EINHEITLICHE AUSWAHLFARBE\n *\n * Gilt für:\n * - einen Tag\n * - mehrere Tage\n * - einen Timeslot\n *\n * Wichtig: FullCalendar verwendet background.\n */\n#calendar .fc .fc-highlight,\n#calendar .fc .fc-highlight-skeleton,\n#calendar .fc .fc-highlight-skeleton td,\n#calendar .fc .fc-highlight-container,\n#calendar .fc .fc-highlight-container td {\n background: var(--calendar-selection-color) !important;\n background-color: var(--calendar-selection-color) !important;\n opacity: 1 !important;\n border: 0 !important;\n box-shadow: none !important;\n}\n\n/*\n * Tagesansicht:\n * mögliche Auswahlcontainer ebenfalls\n * auf dieselbe Farbe setzen.\n */\n#calendar .fc-timegrid .fc-timegrid-col-bg,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container table,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container td,\n#calendar .fc-timegrid .fc-highlight-skeleton,\n#calendar .fc-timegrid .fc-highlight-container {\n background: transparent !important;\n background-color: transparent !important;\n}\n\n/*\n * Die tatsächliche Auswahlfläche\n * in der Tagesansicht.\n */\n#calendar .fc-timegrid .fc-highlight,\n#calendar .fc-timegrid .fc-highlight-skeleton td,\n#calendar .fc-timegrid .fc-highlight-container td {\n background: var(--calendar-selection-color) !important;\n background-color: var(--calendar-selection-color) !important;\n opacity: 1 !important;\n border: 0 !important;\n box-shadow: none !important;\n}\n\n/*\n * Aktueller Tag in der Monatsansicht:\n * transparenter Hintergrund,\n * nur schmaler roter Rahmen.\n */\n#calendar .fc-daygrid-day.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: inset 0 0 0 1px #dc2626 !important;\n}\n\n#calendar .fc-daygrid-day.fc-day-today .fc-daygrid-day-frame {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n}\n\n/*\n * Kopfzelle des aktuellen Tages\n * ohne Hintergrund und ohne Rahmen.\n */\n#calendar .fc-col-header-cell.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n}\n\n/*\n * Tagesansicht:\n * aktueller Tag ohne Hintergrund\n * und ohne roten Rahmen.\n */\n#calendar .fc-timegrid-col.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n\n#calendar .fc-timegrid-col.fc-day-today .fc-timegrid-col-frame {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n\n/* Wochenenden grau */\n#calendar td.fc-day-sat,\n#calendar td.fc-day-sun,\n#calendar .fc-timegrid-col.fc-day-sat,\n#calendar .fc-timegrid-col.fc-day-sun {\n background-color: #eeeeee !important;\n}\n\n/* Wochenend-Kopfzellen */\n#calendar .fc-col-header-cell.fc-day-sat,\n#calendar .fc-col-header-cell.fc-day-sun {\n background-color: #d6d6d6 !important;\n}\n\n/*\n * Monatsansicht:\n * kein Scrollen innerhalb des Monats.\n */\n#calendar .fc-daygrid .fc-scroller {\n height: auto !important;\n max-height: none !important;\n overflow-y: visible !important;\n overflow-x: visible !important;\n}\n\n/*\n * Tagesansicht:\n * nur das Zeitraster scrollt.\n */\n#calendar .fc-timegrid .fc-scroller {\n overflow-y: auto !important;\n overflow-x: hidden !important;\n overscroll-behavior: contain;\n}\n\n/* TimeGrid-Grundstruktur */\n#calendar .fc-timegrid {\n min-height: 0 !important;\n}\n\n#calendar .fc-timegrid .fc-view-harness {\n min-height: 0 !important;\n}\n\n/* Tabellenbreite */\n#calendar .fc-scrollgrid {\n width: 100%;\n max-width: 100%;\n}\n\n/* Größere 30-Minuten-Zeilen */\n#calendar .fc-timegrid-slot {\n height: 30px !important;\n min-height: 30px !important;\n}\n\n#calendar .fc-timegrid-slot-label {\n height: 30px !important;\n min-height: 30px !important;\n}\n\n/*\n * Falls ein dunkelblauer Mirror-Event\n * weiterhin sichtbar ist.\n *\n * Diese Regeln wirken nur auf temporäre\n * Auswahl-Events, nicht auf echte Buchungen.\n */\n#calendar .fc-event-mirror,\n#calendar .fc-timegrid-event-mirror,\n#calendar .fc-timegrid-event-harness-mirror {\n background: var(--calendar-selection-color) !important;\n background-color: var(--calendar-selection-color) !important;\n border-color: transparent !important;\n color: transparent !important;\n opacity: 1 !important;\n box-shadow: none !important;\n}",
"html": "<div id=\"calendarWrapper\">\n <div id=\"calendarHeader\">\n <div id=\"calendarSeatTitle\">\n Kalender\n </div>\n </div>\n\n <div id=\"calendar\"></div>\n\n <div id=\"calendarSelection\">\n Keine Auswahl\n </div>\n</div>\n\n<link\n rel=\"stylesheet\"\n href=\"https://cdn.jsdelivr.net/npm/[email protected]/index.global.min.css\"\n>\n\n<script\n src=\"https://cdn.jsdelivr.net/npm/[email protected]/index.global.min.js\">\n</script>",
"js": "let calendar = null;\nlet resizeTimer = null;\nlet modelChangeRegistered = false;\nlet resizeRegistered = false;\nconst MONTH_HEIGHT = 620;\nconst MONTH_CONTENT_HEIGHT = 550;\nconst DAY_HEIGHT = 600;\nconst DAY_CONTENT_HEIGHT = 480;\nconst BUSINESS_START_MINUTES = 7 * 60;\nconst BUSINESS_END_MINUTES = 18 * 60;\nconst SLOT_MINUTES = 30;\nfunction getModel() {\n const model = appsmith.model || {};\n if (typeof model === \"string\") {\n try {\n return JSON.parse(model);\n } catch (error) {\n return {};\n }\n }\n return model;\n}\nfunction pad(value) {\n return String(value).padStart(2, \"0\");\n}\nfunction formatDate(date) {\n return [date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join(\"-\");\n}\nfunction formatLocalDateTime(date) {\n return `${formatDate(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;\n}\nfunction getDateOnly(value) {\n return String(value || \"\").replace(\"T\", \" \").slice(0, 10);\n}\nfunction dateFromDateOnly(value) {\n const parts = getDateOnly(value).split(\"-\").map(Number);\n return new Date(parts[0], parts[1] - 1, parts[2]);\n}\nfunction isWeekendDate(date) {\n return date.getDay() === 0 || date.getDay() === 6;\n}\nfunction isWeekend(date) {\n return isWeekendDate(date);\n}\nfunction isBusinessDay(date) {\n return date.getDay() >= 1 && date.getDay() <= 5;\n}\nfunction getMinutesOfDay(date) {\n return date.getHours() * 60 + date.getMinutes();\n}\nfunction getWeekdaysBetween(startDate, exclusiveEndDate) {\n const result = [];\n const current = new Date(startDate);\n while (current < exclusiveEndDate) {\n if (!isWeekendDate(current)) result.push(formatDate(current));\n current.setDate(current.getDate() + 1);\n }\n return result;\n}\nfunction makeAllDaySegment(booking, start, end) {\n return {\n id: String(booking.id),\n title: booking.title || booking.titel || \"Platzbuchung\",\n start: `${formatDate(start)}T00:00:00`,\n end: `${formatDate(end)}T00:00:00`,\n allDay: true,\n extendedProps: {\n id: String(booking.id),\n originalBookingId: String(booking.id),\n platz_id: booking.platz_id,\n gebucht_von: booking.gebucht_von,\n allDay: true\n }\n };\n}\nfunction createAllDayEvent(booking) {\n const startDate = getDateOnly(booking.start);\n const endDate = getDateOnly(booking.end);\n const start = dateFromDateOnly(startDate);\n const exclusiveEnd = dateFromDateOnly(endDate);\n const rawEnd = String(booking.end || \"\");\n if (rawEnd.indexOf(\"23:59:59\") >= 0 || exclusiveEnd.getTime() === start.getTime()) {\n exclusiveEnd.setDate(exclusiveEnd.getDate() + 1);\n }\n const weekdays = getWeekdaysBetween(start, exclusiveEnd);\n if (!weekdays.length) return [];\n const events = [];\n let segmentStart = dateFromDateOnly(weekdays[0]);\n let previous = new Date(segmentStart);\n for (let index = 1; index < weekdays.length; index += 1) {\n const current = dateFromDateOnly(weekdays[index]);\n const expected = new Date(previous);\n expected.setDate(expected.getDate() + 1);\n if (current.getTime() !== expected.getTime()) {\n const segmentEnd = new Date(previous);\n segmentEnd.setDate(segmentEnd.getDate() + 1);\n events.push(makeAllDaySegment(booking, segmentStart, segmentEnd));\n segmentStart = new Date(current);\n }\n previous = new Date(current);\n }\n const finalEnd = new Date(previous);\n finalEnd.setDate(finalEnd.getDate() + 1);\n events.push(makeAllDaySegment(booking, segmentStart, finalEnd));\n return events;\n}\nfunction createCalendarEvents(bookings, selectedSeatId) {\n return bookings.filter(booking => {\n const bookingSeatId = String(booking.platz_id || booking.extendedProps && booking.extendedProps.platz_id || \"\").trim();\n return bookingSeatId === String(selectedSeatId).trim();\n }).reduce((result, booking) => {\n const allDay = booking.allDay === true || booking.allDay === \"true\" || booking.startTime == null && booking.endTime == null;\n if (allDay) return result.concat(createAllDayEvent(booking));\n result.push({\n id: String(booking.id),\n title: booking.title || booking.titel || \"Platzbuchung\",\n start: String(booking.start || \"\").replace(\" \", \"T\").replace(/Z$/, \"\"),\n end: String(booking.end || \"\").replace(\" \", \"T\").replace(/Z$/, \"\"),\n allDay: false,\n extendedProps: {\n id: String(booking.id),\n originalBookingId: String(booking.id),\n platz_id: booking.platz_id,\n gebucht_von: booking.gebucht_von,\n allDay: false\n }\n });\n return result;\n }, []);\n}\nfunction getEvents() {\n const model = getModel();\n const selectedSeatId = String(model.seatId || model.selectedSeatId || \"\").trim();\n const bookings = Array.isArray(model.events) ? model.events : [];\n return createCalendarEvents(bookings, selectedSeatId);\n}\nfunction getSeatName() {\n const model = getModel();\n return model.seatName || model.seatId || \"Kein Platz ausgewählt\";\n}\nfunction getCurrentUser() {\n const model = getModel();\n return String(model.currentUser || \"\").trim().toLowerCase();\n}\nfunction updateSeatTitle() {\n const element = document.getElementById(\"calendarSeatTitle\");\n if (element) element.textContent = `Kalender für ${getSeatName()}`;\n}\nfunction isDayView() {\n return Boolean(calendar && calendar.view && calendar.view.type === \"timeGridDay\");\n}\nfunction updateSelectionText(text) {\n const element = document.getElementById(\"calendarSelection\");\n if (!element) return;\n if (isDayView()) {\n element.textContent = \"\";\n element.style.display = \"none\";\n return;\n }\n element.style.display = \"block\";\n element.textContent = text;\n}\nfunction updateCalendarViewSettings(viewType) {\n if (!calendar) return;\n const dayView = viewType === \"timeGridDay\";\n calendar.setOption(\"height\", dayView ? DAY_HEIGHT : MONTH_HEIGHT);\n calendar.setOption(\"contentHeight\", dayView ? DAY_CONTENT_HEIGHT : MONTH_CONTENT_HEIGHT);\n calendar.setOption(\"expandRows\", !dayView);\n if (typeof calendar.updateSize === \"function\") calendar.updateSize();\n}\nfunction scheduleResize() {\n window.clearTimeout(resizeTimer);\n resizeTimer = window.setTimeout(() => {\n if (calendar) calendar.updateSize();\n }, 250);\n}\nfunction isBusinessTimeRange(start, end) {\n if (!start || !end || !isBusinessDay(start)) return false;\n if (start.toDateString() !== end.toDateString()) return false;\n return getMinutesOfDay(start) >= BUSINESS_START_MINUTES && getMinutesOfDay(end) <= BUSINESS_END_MINUTES && getMinutesOfDay(end) > getMinutesOfDay(start);\n}\nfunction isValidAllDaySelection(start, end) {\n return Boolean(start && end && end > start);\n}\nfunction showWeekendMessage() {\n updateSelectionText(\"Samstag und Sonntag sind nicht buchbar.\");\n}\nfunction showBusinessHoursMessage() {\n updateSelectionText(\"Reservierungen sind Montag bis Freitag von 07:00 bis 18:00 Uhr möglich.\");\n}\nfunction openDayView(date) {\n if (!calendar) return;\n if (isWeekend(date)) {\n showWeekendMessage();\n return;\n }\n calendar.changeView(\"timeGridDay\", date);\n}\nfunction isDayNumberClick(info) {\n const target = info.jsEvent && info.jsEvent.target;\n return Boolean(target && typeof target.closest === \"function\" && target.closest(\".fc-daygrid-day-number\"));\n}\nfunction roundToHalfHour(date) {\n const result = new Date(date);\n result.setSeconds(0, 0);\n result.setMinutes(result.getMinutes() < 30 ? 0 : 30);\n return result;\n}\nfunction formatDateTime(date) {\n if (!date) return \"\";\n return new Intl.DateTimeFormat(\"de-DE\", {\n dateStyle: \"short\",\n timeStyle: \"short\"\n }).format(date);\n}\nfunction triggerAppsmithEvent(eventName) {\n try {\n appsmith.triggerEvent(eventName);\n } catch (error) {\n console.warn(`Event ${eventName} ist nicht konfiguriert.`, error);\n }\n}\nfunction normalizeBookingEvent(event) {\n const extended = event.extendedProps || {};\n return {\n id: extended.originalBookingId || extended.id || event.id || null,\n platz_id: extended.platz_id || event.platz_id || null,\n title: event.title || extended.title || \"Platzbuchung\",\n start: event.start ? formatLocalDateTime(event.start) : null,\n end: event.end ? formatLocalDateTime(event.end) : null,\n gebucht_von: extended.gebucht_von || event.gebucht_von || null,\n allDay: event.allDay === true || extended.allDay === true\n };\n}\nfunction isOwnBooking(booking) {\n const currentUser = getCurrentUser();\n const bookingUser = String(booking.gebucht_von || \"\").trim().toLowerCase();\n return Boolean(currentUser && bookingUser && currentUser === bookingUser);\n}\nfunction handleBookingClick(info) {\n const booking = normalizeBookingEvent(info.event);\n if (!booking.id) {\n updateSelectionText(\"Diese Buchung besitzt keine ID und kann nicht gelöscht werden.\");\n return;\n }\n if (!isOwnBooking(booking)) {\n triggerAppsmithEvent(\"foreignBookingClicked\");\n updateSelectionText(\"Diese Buchung gehört einem anderen Benutzer.\");\n return;\n }\n appsmith.updateModel({\n selectedBooking: booking\n });\n updateSelectionText(`${booking.title} ${info.event.allDay ? \"Ganztägig\" : formatDateTime(info.event.start)}`);\n triggerAppsmithEvent(\"bookingClicked\");\n}\nfunction buildCalendarSelection(type, start, end) {\n const allDay = type === \"day\" || type === \"days\";\n return {\n type: String(type),\n start: formatLocalDateTime(start),\n end: formatLocalDateTime(end),\n allDay,\n selectedDays: allDay ? Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)) : null\n };\n}\nfunction publishCalendarSelection(type, start, end) {\n if (!start || !end) return;\n appsmith.updateModel({\n calendarSelection: buildCalendarSelection(type, start, end)\n });\n if (type === \"day\") updateSelectionText(`Ausgewählter Tag: ${formatDateTime(start)}`);else if (type === \"days\") updateSelectionText(`Ausgewählte Tage: ${formatDateTime(start)} bis ${formatDateTime(new Date(end.getTime() - 1))}`);else updateSelectionText(`Ausgewählt: ${formatDateTime(start)} bis ${formatDateTime(end)}`);\n triggerAppsmithEvent(type === \"time\" ? \"timeSelected\" : \"bookingSelected\");\n}\nfunction selectSingleMonthDay(date) {\n if (!calendar || !date) return;\n if (isWeekend(date)) {\n calendar.unselect();\n showWeekendMessage();\n return;\n }\n const start = new Date(date);\n start.setHours(0, 0, 0, 0);\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n calendar.unselect();\n window.setTimeout(() => {\n if (calendar) calendar.select({\n start,\n end,\n allDay: true\n });\n }, 0);\n}\nfunction createCalendar() {\n const calendarElement = document.getElementById(\"calendar\");\n if (!calendarElement) return;\n if (typeof FullCalendar === \"undefined\") {\n calendarElement.textContent = \"FullCalendar konnte nicht geladen werden.\";\n return;\n }\n if (calendar) {\n calendar.destroy();\n calendar = null;\n }\n calendar = new FullCalendar.Calendar(calendarElement, {\n initialView: \"dayGridMonth\",\n timeZone: \"local\",\n locale: \"de\",\n firstDay: 1,\n displayEventTime: false,\n allDaySlot: true,\n businessHours: {\n daysOfWeek: [1, 2, 3, 4, 5],\n startTime: \"07:00\",\n endTime: \"18:00\"\n },\n navLinks: true,\n navLinkDayClick(date) {\n openDayView(date);\n },\n dateClick(info) {\n const clickedDate = info.date;\n if (info.view.type === \"dayGridMonth\") {\n if (isDayNumberClick(info)) {\n openDayView(clickedDate);\n return;\n }\n selectSingleMonthDay(clickedDate);\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n if (isWeekend(clickedDate)) {\n showWeekendMessage();\n return;\n }\n const startSlot = roundToHalfHour(clickedDate);\n const endSlot = new Date(startSlot.getTime() + SLOT_MINUTES * 60 * 1000);\n if (!isBusinessTimeRange(startSlot, endSlot)) {\n showBusinessHoursMessage();\n return;\n }\n calendar.unselect();\n calendar.select({\n start: startSlot,\n end: endSlot,\n allDay: false\n });\n }\n },\n selectable: true,\n selectMirror: false,\n unselectAuto: false,\n selectMinDistance: 5,\n selectAllow(info) {\n if (info.allDay) return isValidAllDaySelection(info.start, info.end);\n return isBusinessTimeRange(info.start, info.end);\n },\n editable: false,\n eventStartEditable: false,\n eventDurationEditable: false,\n select(info) {\n if (info.view.type === \"dayGridMonth\") {\n const days = Math.round((info.end.getTime() - info.start.getTime()) / (24 * 60 * 60 * 1000));\n publishCalendarSelection(days === 1 ? \"day\" : \"days\", info.start, info.end);\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n if (!isBusinessTimeRange(info.start, info.end)) {\n calendar.unselect();\n showBusinessHoursMessage();\n return;\n }\n publishCalendarSelection(\"time\", info.start, info.end);\n }\n },\n unselect() {\n const element = document.getElementById(\"calendarSelection\");\n if (isDayView()) {\n if (element) {\n element.textContent = \"\";\n element.style.display = \"none\";\n }\n return;\n }\n if (element) {\n element.style.display = \"block\";\n element.textContent = \"Keine Auswahl\";\n }\n appsmith.updateModel({\n calendarSelection: null\n });\n },\n height: MONTH_HEIGHT,\n contentHeight: MONTH_CONTENT_HEIGHT,\n expandRows: true,\n headerToolbar: {\n left: \"prev,next today\",\n center: \"title\",\n right: \"dayGridMonth,timeGridDay\"\n },\n buttonText: {\n today: \"Heute\",\n month: \"Monat\",\n day: \"Tag\"\n },\n slotDuration: \"00:30:00\",\n snapDuration: \"00:30:00\",\n slotMinTime: \"07:00:00\",\n slotMaxTime: \"22:00:00\",\n scrollTime: \"08:00:00\",\n scrollTimeReset: false,\n nowIndicator: true,\n events: getEvents(),\n eventClick(info) {\n handleBookingClick(info);\n },\n eventDidMount(info) {\n const booking = normalizeBookingEvent(info.event);\n info.el.style.cursor = booking.id && isOwnBooking(booking) ? \"pointer\" : \"default\";\n if (booking.id && isOwnBooking(booking)) info.el.title = \"Eigene Buchung anklicken, um Details zu öffnen\";\n },\n datesSet(info) {\n updateCalendarViewSettings(info.view.type);\n }\n });\n calendar.render();\n updateCalendarViewSettings(calendar.view.type);\n}\nfunction refreshCalendarEvents() {\n if (!calendar) {\n createCalendar();\n return;\n }\n calendar.removeAllEvents();\n calendar.addEventSource(getEvents());\n calendar.updateSize();\n}\nfunction registerResizeHandling() {\n if (resizeRegistered) return;\n resizeRegistered = true;\n window.addEventListener(\"resize\", scheduleResize);\n}\nappsmith.onReady(() => {\n updateSeatTitle();\n createCalendar();\n registerResizeHandling();\n if (!modelChangeRegistered) {\n modelChangeRegistered = true;\n appsmith.onModelChange(() => {\n updateSeatTitle();\n refreshCalendarEvents();\n });\n }\n});"
},
"theme": "{{appsmith.theme}}",
"timeSelected": "{{\n BookingSelect.openBookingWidget()\n}}",
"topRow": 0,
"type": "CUSTOM_WIDGET",
"uncompiledSrcDoc": {
"css": "html,\nbody {\n margin: 0;\n padding: 0;\n width: 100%;\n min-height: 100%;\n font-family: Arial, sans-serif;\n overflow: hidden;\n}\n\n/* Äußerer Custom-Widget-Bereich */\n\n#calendarWrapper {\n width: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: 10px;\n background: #ffffff;\n overflow: visible;\n}\n\n/* Kopfbereich */\n\n#calendarHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 10px;\n padding: 8px 10px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n background: #f9fafb;\n}\n\n#calendarSeatTitle {\n font-size: 15px;\n font-weight: 600;\n color: #374151;\n}\n\n/* Kalender */\n\n#calendar {\n width: 100%;\n min-height: 0;\n box-sizing: border-box;\n\n --calendar-selection-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n\n --fc-highlight-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n\n --fc-today-bg-color: transparent;\n}\n\n/* Auswahltext */\n\n#calendarSelection {\n margin-top: 10px;\n padding: 8px 10px;\n border: 1px solid #d1d5db;\n border-radius: 5px;\n background: #f9fafb;\n color: #374151;\n font-size: 13px;\n}\n\n/* FullCalendar-Grundformatierung */\n\n#calendar .fc {\n width: 100%;\n font-size: 13px;\n\n --fc-highlight-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n\n --fc-today-bg-color: transparent;\n}\n\n#calendar .fc .fc-button {\n background: #374151;\n border-color: #374151;\n}\n\n#calendar .fc .fc-button:hover,\n#calendar .fc .fc-button-active {\n background: #111827;\n border-color: #111827;\n}\n\n#calendar .fc .fc-event {\n cursor: pointer;\n}\n\n/*\n * EINHEITLICHE AUSWAHLFARBE\n *\n * Gilt für:\n * - einen Tag\n * - mehrere Tage\n * - einen Timeslot\n *\n * Wichtig: FullCalendar verwendet background.\n */\n#calendar .fc .fc-highlight,\n#calendar .fc .fc-highlight-skeleton,\n#calendar .fc .fc-highlight-skeleton td,\n#calendar .fc .fc-highlight-container,\n#calendar .fc .fc-highlight-container td {\n background: var(\n --calendar-selection-color\n ) !important;\n\n background-color: var(\n --calendar-selection-color\n ) !important;\n\n opacity: 1 !important;\n border: 0 !important;\n box-shadow: none !important;\n}\n\n/*\n * Tagesansicht:\n * mögliche Auswahlcontainer ebenfalls\n * auf dieselbe Farbe setzen.\n */\n#calendar .fc-timegrid .fc-timegrid-col-bg,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container table,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container td,\n#calendar .fc-timegrid .fc-highlight-skeleton,\n#calendar .fc-timegrid .fc-highlight-container {\n background: transparent !important;\n background-color: transparent !important;\n}\n\n/*\n * Die tatsächliche Auswahlfläche\n * in der Tagesansicht.\n */\n#calendar .fc-timegrid .fc-highlight,\n#calendar .fc-timegrid .fc-highlight-skeleton td,\n#calendar .fc-timegrid .fc-highlight-container td {\n background: var(\n --calendar-selection-color\n ) !important;\n\n background-color: var(\n --calendar-selection-color\n ) !important;\n\n opacity: 1 !important;\n border: 0 !important;\n box-shadow: none !important;\n}\n\n/*\n * Aktueller Tag in der Monatsansicht:\n * transparenter Hintergrund,\n * nur schmaler roter Rahmen.\n */\n#calendar .fc-daygrid-day.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: inset 0 0 0 1px #dc2626 !important;\n}\n\n#calendar\n .fc-daygrid-day.fc-day-today\n .fc-daygrid-day-frame {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n}\n\n/*\n * Kopfzelle des aktuellen Tages\n * ohne Hintergrund und ohne Rahmen.\n */\n#calendar .fc-col-header-cell.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n}\n\n/*\n * Tagesansicht:\n * aktueller Tag ohne Hintergrund\n * und ohne roten Rahmen.\n */\n#calendar .fc-timegrid-col.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n\n#calendar\n .fc-timegrid-col.fc-day-today\n .fc-timegrid-col-frame {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n\n/* Wochenenden grau */\n\n#calendar td.fc-day-sat,\n#calendar td.fc-day-sun,\n#calendar .fc-timegrid-col.fc-day-sat,\n#calendar .fc-timegrid-col.fc-day-sun {\n background-color: #eeeeee !important;\n}\n\n/* Wochenend-Kopfzellen */\n\n#calendar .fc-col-header-cell.fc-day-sat,\n#calendar .fc-col-header-cell.fc-day-sun {\n background-color: #d6d6d6 !important;\n}\n\n/*\n * Monatsansicht:\n * kein Scrollen innerhalb des Monats.\n */\n#calendar .fc-daygrid .fc-scroller {\n height: auto !important;\n max-height: none !important;\n overflow-y: visible !important;\n overflow-x: visible !important;\n}\n\n/*\n * Tagesansicht:\n * nur das Zeitraster scrollt.\n */\n#calendar .fc-timegrid .fc-scroller {\n overflow-y: auto !important;\n overflow-x: hidden !important;\n overscroll-behavior: contain;\n}\n\n/* TimeGrid-Grundstruktur */\n\n#calendar .fc-timegrid {\n min-height: 0 !important;\n}\n\n#calendar .fc-timegrid .fc-view-harness {\n min-height: 0 !important;\n}\n\n/* Tabellenbreite */\n\n#calendar .fc-scrollgrid {\n width: 100%;\n max-width: 100%;\n}\n\n/* Größere 30-Minuten-Zeilen */\n\n#calendar .fc-timegrid-slot {\n height: 30px !important;\n min-height: 30px !important;\n}\n\n#calendar .fc-timegrid-slot-label {\n height: 30px !important;\n min-height: 30px !important;\n}\n\n/*\n * Falls ein dunkelblauer Mirror-Event\n * weiterhin sichtbar ist.\n *\n * Diese Regeln wirken nur auf temporäre\n * Auswahl-Events, nicht auf echte Buchungen.\n */\n#calendar .fc-event-mirror,\n#calendar .fc-timegrid-event-mirror,\n#calendar .fc-timegrid-event-harness-mirror {\n background: var(\n --calendar-selection-color\n ) !important;\n\n background-color: var(\n --calendar-selection-color\n ) !important;\n\n border-color: transparent !important;\n color: transparent !important;\n opacity: 1 !important;\n box-shadow: none !important;\n}",
"html": "<div id=\"calendarWrapper\">\n <div id=\"calendarHeader\">\n <div id=\"calendarSeatTitle\">\n Kalender\n </div>\n </div>\n\n <div id=\"calendar\"></div>\n\n <div id=\"calendarSelection\">\n Keine Auswahl\n </div>\n</div>\n\n<link\n rel=\"stylesheet\"\n href=\"https://cdn.jsdelivr.net/npm/[email protected]/index.global.min.css\"\n>\n\n<script\n src=\"https://cdn.jsdelivr.net/npm/[email protected]/index.global.min.js\">\n</script>",
"js": "let calendar = null;\nlet resizeTimer = null;\nlet modelChangeRegistered = false;\nlet resizeRegistered = false;\n\nconst MONTH_HEIGHT = 620;\nconst MONTH_CONTENT_HEIGHT = 550;\nconst DAY_HEIGHT = 600;\nconst DAY_CONTENT_HEIGHT = 480;\nconst BUSINESS_START_MINUTES = 7 * 60;\nconst BUSINESS_END_MINUTES = 18 * 60;\nconst SLOT_MINUTES = 30;\n\nfunction getModel() {\n const model = appsmith.model || {};\n if (typeof model === \"string\") {\n try { return JSON.parse(model); } catch (error) { return {}; }\n }\n return model;\n}\n\nfunction pad(value) { return String(value).padStart(2, \"0\"); }\nfunction formatDate(date) { return [date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join(\"-\"); }\nfunction formatLocalDateTime(date) { return `${formatDate(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }\nfunction getDateOnly(value) { return String(value || \"\").replace(\"T\", \" \").slice(0, 10); }\nfunction dateFromDateOnly(value) { const parts = getDateOnly(value).split(\"-\").map(Number); return new Date(parts[0], parts[1] - 1, parts[2]); }\nfunction isWeekendDate(date) { return date.getDay() === 0 || date.getDay() === 6; }\nfunction isWeekend(date) { return isWeekendDate(date); }\nfunction isBusinessDay(date) { return date.getDay() >= 1 && date.getDay() <= 5; }\nfunction getMinutesOfDay(date) { return date.getHours() * 60 + date.getMinutes(); }\n\nfunction getWeekdaysBetween(startDate, exclusiveEndDate) {\n const result = [];\n const current = new Date(startDate);\n while (current < exclusiveEndDate) {\n if (!isWeekendDate(current)) result.push(formatDate(current));\n current.setDate(current.getDate() + 1);\n }\n return result;\n}\n\nfunction makeAllDaySegment(booking, start, end) {\n return {\n id: String(booking.id),\n title: booking.title || booking.titel || \"Platzbuchung\",\n start: `${formatDate(start)}T00:00:00`,\n end: `${formatDate(end)}T00:00:00`,\n allDay: true,\n extendedProps: {\n id: String(booking.id),\n originalBookingId: String(booking.id),\n platz_id: booking.platz_id,\n gebucht_von: booking.gebucht_von,\n allDay: true\n }\n };\n}\n\nfunction createAllDayEvent(booking) {\n const startDate = getDateOnly(booking.start);\n const endDate = getDateOnly(booking.end);\n const start = dateFromDateOnly(startDate);\n const exclusiveEnd = dateFromDateOnly(endDate);\n const rawEnd = String(booking.end || \"\");\n\n if (rawEnd.indexOf(\"23:59:59\") >= 0 || exclusiveEnd.getTime() === start.getTime()) {\n exclusiveEnd.setDate(exclusiveEnd.getDate() + 1);\n }\n\n const weekdays = getWeekdaysBetween(start, exclusiveEnd);\n if (!weekdays.length) return [];\n\n const events = [];\n let segmentStart = dateFromDateOnly(weekdays[0]);\n let previous = new Date(segmentStart);\n\n for (let index = 1; index < weekdays.length; index += 1) {\n const current = dateFromDateOnly(weekdays[index]);\n const expected = new Date(previous);\n expected.setDate(expected.getDate() + 1);\n\n if (current.getTime() !== expected.getTime()) {\n const segmentEnd = new Date(previous);\n segmentEnd.setDate(segmentEnd.getDate() + 1);\n events.push(makeAllDaySegment(booking, segmentStart, segmentEnd));\n segmentStart = new Date(current);\n }\n\n previous = new Date(current);\n }\n\n const finalEnd = new Date(previous);\n finalEnd.setDate(finalEnd.getDate() + 1);\n events.push(makeAllDaySegment(booking, segmentStart, finalEnd));\n return events;\n}\n\nfunction createCalendarEvents(bookings, selectedSeatId) {\n return bookings\n .filter((booking) => {\n const bookingSeatId = String(\n booking.platz_id ||\n (booking.extendedProps && booking.extendedProps.platz_id) ||\n \"\"\n ).trim();\n return bookingSeatId === String(selectedSeatId).trim();\n })\n .reduce((result, booking) => {\n const allDay = booking.allDay === true || booking.allDay === \"true\" || (booking.startTime == null && booking.endTime == null);\n if (allDay) return result.concat(createAllDayEvent(booking));\n\n result.push({\n id: String(booking.id),\n title: booking.title || booking.titel || \"Platzbuchung\",\n start: String(booking.start || \"\").replace(\" \", \"T\").replace(/Z$/, \"\"),\n end: String(booking.end || \"\").replace(\" \", \"T\").replace(/Z$/, \"\"),\n allDay: false,\n extendedProps: {\n id: String(booking.id),\n originalBookingId: String(booking.id),\n platz_id: booking.platz_id,\n gebucht_von: booking.gebucht_von,\n allDay: false\n }\n });\n return result;\n }, []);\n}\n\nfunction getEvents() {\n const model = getModel();\n const selectedSeatId = String(model.seatId || model.selectedSeatId || \"\").trim();\n const bookings = Array.isArray(model.events) ? model.events : [];\n return createCalendarEvents(bookings, selectedSeatId);\n}\n\nfunction getSeatName() { const model = getModel(); return model.seatName || model.seatId || \"Kein Platz ausgewählt\"; }\nfunction getCurrentUser() { const model = getModel(); return String(model.currentUser || \"\").trim().toLowerCase(); }\nfunction updateSeatTitle() { const element = document.getElementById(\"calendarSeatTitle\"); if (element) element.textContent = `Kalender für ${getSeatName()}`; }\nfunction isDayView() { return Boolean(calendar && calendar.view && calendar.view.type === \"timeGridDay\"); }\n\nfunction updateSelectionText(text) {\n const element = document.getElementById(\"calendarSelection\");\n if (!element) return;\n if (isDayView()) { element.textContent = \"\"; element.style.display = \"none\"; return; }\n element.style.display = \"block\";\n element.textContent = text;\n}\n\nfunction updateCalendarViewSettings(viewType) {\n if (!calendar) return;\n const dayView = viewType === \"timeGridDay\";\n calendar.setOption(\"height\", dayView ? DAY_HEIGHT : MONTH_HEIGHT);\n calendar.setOption(\"contentHeight\", dayView ? DAY_CONTENT_HEIGHT : MONTH_CONTENT_HEIGHT);\n calendar.setOption(\"expandRows\", !dayView);\n if (typeof calendar.updateSize === \"function\") calendar.updateSize();\n}\n\nfunction scheduleResize() {\n window.clearTimeout(resizeTimer);\n resizeTimer = window.setTimeout(() => { if (calendar) calendar.updateSize(); }, 250);\n}\n\nfunction isBusinessTimeRange(start, end) {\n if (!start || !end || !isBusinessDay(start)) return false;\n if (start.toDateString() !== end.toDateString()) return false;\n return getMinutesOfDay(start) >= BUSINESS_START_MINUTES && getMinutesOfDay(end) <= BUSINESS_END_MINUTES && getMinutesOfDay(end) > getMinutesOfDay(start);\n}\n\nfunction isValidAllDaySelection(start, end) {\n return Boolean(start && end && end > start);\n}\n\nfunction showWeekendMessage() { updateSelectionText(\"Samstag und Sonntag sind nicht buchbar.\"); }\nfunction showBusinessHoursMessage() { updateSelectionText(\"Reservierungen sind Montag bis Freitag von 07:00 bis 18:00 Uhr möglich.\"); }\nfunction openDayView(date) { if (!calendar) return; if (isWeekend(date)) { showWeekendMessage(); return; } calendar.changeView(\"timeGridDay\", date); }\n\nfunction isDayNumberClick(info) {\n const target = info.jsEvent && info.jsEvent.target;\n return Boolean(target && typeof target.closest === \"function\" && target.closest(\".fc-daygrid-day-number\"));\n}\n\nfunction roundToHalfHour(date) {\n const result = new Date(date);\n result.setSeconds(0, 0);\n result.setMinutes(result.getMinutes() < 30 ? 0 : 30);\n return result;\n}\n\nfunction formatDateTime(date) {\n if (!date) return \"\";\n return new Intl.DateTimeFormat(\"de-DE\", { dateStyle: \"short\", timeStyle: \"short\" }).format(date);\n}\n\nfunction triggerAppsmithEvent(eventName) {\n try { appsmith.triggerEvent(eventName); } catch (error) { console.warn(`Event ${eventName} ist nicht konfiguriert.`, error); }\n}\n\nfunction normalizeBookingEvent(event) {\n const extended = event.extendedProps || {};\n return {\n id: extended.originalBookingId || extended.id || event.id || null,\n platz_id: extended.platz_id || event.platz_id || null,\n title: event.title || extended.title || \"Platzbuchung\",\n start: event.start ? formatLocalDateTime(event.start) : null,\n end: event.end ? formatLocalDateTime(event.end) : null,\n gebucht_von: extended.gebucht_von || event.gebucht_von || null,\n allDay: event.allDay === true || extended.allDay === true\n };\n}\n\nfunction isOwnBooking(booking) {\n const currentUser = getCurrentUser();\n const bookingUser = String(booking.gebucht_von || \"\").trim().toLowerCase();\n return Boolean(currentUser && bookingUser && currentUser === bookingUser);\n}\n\nfunction handleBookingClick(info) {\n const booking = normalizeBookingEvent(info.event);\n if (!booking.id) { updateSelectionText(\"Diese Buchung besitzt keine ID und kann nicht gelöscht werden.\"); return; }\n if (!isOwnBooking(booking)) { triggerAppsmithEvent(\"foreignBookingClicked\"); updateSelectionText(\"Diese Buchung gehört einem anderen Benutzer.\"); return; }\n appsmith.updateModel({ selectedBooking: booking });\n updateSelectionText(`${booking.title} ${info.event.allDay ? \"Ganztägig\" : formatDateTime(info.event.start)}`);\n triggerAppsmithEvent(\"bookingClicked\");\n}\n\nfunction buildCalendarSelection(type, start, end) {\n const allDay = type === \"day\" || type === \"days\";\n return { type: String(type), start: formatLocalDateTime(start), end: formatLocalDateTime(end), allDay, selectedDays: allDay ? Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)) : null };\n}\n\nfunction publishCalendarSelection(type, start, end) {\n if (!start || !end) return;\n appsmith.updateModel({ calendarSelection: buildCalendarSelection(type, start, end) });\n if (type === \"day\") updateSelectionText(`Ausgewählter Tag: ${formatDateTime(start)}`);\n else if (type === \"days\") updateSelectionText(`Ausgewählte Tage: ${formatDateTime(start)} bis ${formatDateTime(new Date(end.getTime() - 1))}`);\n else updateSelectionText(`Ausgewählt: ${formatDateTime(start)} bis ${formatDateTime(end)}`);\n triggerAppsmithEvent(type === \"time\" ? \"timeSelected\" : \"bookingSelected\");\n}\n\nfunction selectSingleMonthDay(date) {\n if (!calendar || !date) return;\n if (isWeekend(date)) { calendar.unselect(); showWeekendMessage(); return; }\n const start = new Date(date); start.setHours(0, 0, 0, 0);\n const end = new Date(start); end.setDate(end.getDate() + 1);\n calendar.unselect();\n window.setTimeout(() => { if (calendar) calendar.select({ start, end, allDay: true }); }, 0);\n}\n\nfunction createCalendar() {\n const calendarElement = document.getElementById(\"calendar\");\n if (!calendarElement) return;\n if (typeof FullCalendar === \"undefined\") { calendarElement.textContent = \"FullCalendar konnte nicht geladen werden.\"; return; }\n if (calendar) { calendar.destroy(); calendar = null; }\n\n calendar = new FullCalendar.Calendar(calendarElement, {\n initialView: \"dayGridMonth\",\n timeZone: \"local\",\n locale: \"de\",\n firstDay: 1,\n displayEventTime: false,\n allDaySlot: true,\n businessHours: { daysOfWeek: [1, 2, 3, 4, 5], startTime: \"07:00\", endTime: \"18:00\" },\n navLinks: true,\n navLinkDayClick(date) { openDayView(date); },\n\n dateClick(info) {\n const clickedDate = info.date;\n if (info.view.type === \"dayGridMonth\") {\n if (isDayNumberClick(info)) { openDayView(clickedDate); return; }\n selectSingleMonthDay(clickedDate);\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n if (isWeekend(clickedDate)) { showWeekendMessage(); return; }\n const startSlot = roundToHalfHour(clickedDate);\n const endSlot = new Date(startSlot.getTime() + SLOT_MINUTES * 60 * 1000);\n if (!isBusinessTimeRange(startSlot, endSlot)) { showBusinessHoursMessage(); return; }\n calendar.unselect();\n calendar.select({ start: startSlot, end: endSlot, allDay: false });\n }\n },\n\n selectable: true,\n selectMirror: false,\n unselectAuto: false,\n selectMinDistance: 5,\n selectAllow(info) {\n if (info.allDay) return isValidAllDaySelection(info.start, info.end);\n return isBusinessTimeRange(info.start, info.end);\n },\n editable: false,\n eventStartEditable: false,\n eventDurationEditable: false,\n\n select(info) {\n if (info.view.type === \"dayGridMonth\") {\n const days = Math.round((info.end.getTime() - info.start.getTime()) / (24 * 60 * 60 * 1000));\n publishCalendarSelection(days === 1 ? \"day\" : \"days\", info.start, info.end);\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n if (!isBusinessTimeRange(info.start, info.end)) { calendar.unselect(); showBusinessHoursMessage(); return; }\n publishCalendarSelection(\"time\", info.start, info.end);\n }\n },\n\n unselect() {\n const element = document.getElementById(\"calendarSelection\");\n if (isDayView()) { if (element) { element.textContent = \"\"; element.style.display = \"none\"; } return; }\n if (element) { element.style.display = \"block\"; element.textContent = \"Keine Auswahl\"; }\n appsmith.updateModel({ calendarSelection: null });\n },\n\n height: MONTH_HEIGHT,\n contentHeight: MONTH_CONTENT_HEIGHT,\n expandRows: true,\n headerToolbar: { left: \"prev,next today\", center: \"title\", right: \"dayGridMonth,timeGridDay\" },\n buttonText: { today: \"Heute\", month: \"Monat\", day: \"Tag\" },\n slotDuration: \"00:30:00\",\n snapDuration: \"00:30:00\",\n slotMinTime: \"07:00:00\",\n slotMaxTime: \"22:00:00\",\n scrollTime: \"08:00:00\",\n scrollTimeReset: false,\n nowIndicator: true,\n events: getEvents(),\n eventClick(info) { handleBookingClick(info); },\n eventDidMount(info) {\n const booking = normalizeBookingEvent(info.event);\n info.el.style.cursor = booking.id && isOwnBooking(booking) ? \"pointer\" : \"default\";\n if (booking.id && isOwnBooking(booking)) info.el.title = \"Eigene Buchung anklicken, um Details zu öffnen\";\n },\n datesSet(info) { updateCalendarViewSettings(info.view.type); }\n });\n\n calendar.render();\n updateCalendarViewSettings(calendar.view.type);\n}\n\nfunction refreshCalendarEvents() {\n if (!calendar) { createCalendar(); return; }\n calendar.removeAllEvents();\n calendar.addEventSource(getEvents());\n calendar.updateSize();\n}\n\nfunction registerResizeHandling() {\n if (resizeRegistered) return;\n resizeRegistered = true;\n window.addEventListener(\"resize\", scheduleResize);\n}\n\nappsmith.onReady(() => {\n updateSeatTitle();\n createCalendar();\n registerResizeHandling();\n if (!modelChangeRegistered) {\n modelChangeRegistered = true;\n appsmith.onModelChange(() => { updateSeatTitle(); refreshCalendarEvents(); });\n }\n});"
},
"version": 1,
"widgetId": "ad7fob2vs9",
"widgetName": "Custom5"
}