Files
Sitzplatzbuchungstool/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json
T
2026-08-19 13:28:52 +00:00

85 lines
39 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",
"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"
}
],
"dynamicTriggerPathList": [
{
"key": "seatSelected"
},
{
"key": "bookingSelected"
},
{
"key": "timeSelected"
}
],
"events": [
"seatSelected",
"bookingSelected",
"timeSelected"
],
"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 \"selectedSeatId\",\n Custom4.model.selectedSeat.id\n )\n .then(() =>\n storeValue(\n \"selectedSeatName\",\n Custom4.model.selectedSeat.name\n )\n )\n .then(() => CreateBooking.run())\n .then(() => showModal(SeatCalendar.name))\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 getEvents() {\n const model = getModel();\n return Array.isArray(model.events) ? model.events : [];\n}\nfunction getSeatName() {\n const model = getModel();\n return model.seatName || model.seatId || \"Kein Platz ausgewählt\";\n}\nfunction updateSeatTitle() {\n const element = document.getElementById(\"calendarSeatTitle\");\n if (!element) {\n return;\n }\n 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) {\n return;\n }\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) {\n return;\n }\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\") {\n calendar.updateSize();\n }\n}\nfunction scheduleResize() {\n window.clearTimeout(resizeTimer);\n resizeTimer = window.setTimeout(() => {\n if (calendar) {\n calendar.updateSize();\n }\n }, 250);\n}\nfunction isWeekend(date) {\n const day = date.getDay();\n return day === 0 || day === 6;\n}\nfunction isBusinessDay(date) {\n const day = date.getDay();\n return day >= 1 && day <= 5;\n}\nfunction getMinutesOfDay(date) {\n return date.getHours() * 60 + date.getMinutes();\n}\nfunction isBusinessTimeRange(start, end) {\n if (!start || !end) {\n return false;\n }\n if (!isBusinessDay(start)) {\n return false;\n }\n if (start.toDateString() !== end.toDateString()) {\n return false;\n }\n const startMinutes = getMinutesOfDay(start);\n const endMinutes = getMinutesOfDay(end);\n return startMinutes >= BUSINESS_START_MINUTES && endMinutes <= BUSINESS_END_MINUTES && endMinutes > startMinutes;\n}\nfunction isBusinessDayRange(start, end) {\n if (!start || !end || end <= start) {\n return false;\n }\n const lastSelectedDay = new Date(end.getTime() - 1);\n const current = new Date(start);\n current.setHours(12, 0, 0, 0);\n lastSelectedDay.setHours(12, 0, 0, 0);\n while (current <= lastSelectedDay) {\n if (!isBusinessDay(current)) {\n return false;\n }\n current.setDate(current.getDate() + 1);\n }\n return true;\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) {\n return;\n }\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 if (!target || typeof target.closest !== \"function\") {\n return false;\n }\n return Boolean(target.closest(\".fc-daygrid-day-number\"));\n}\nfunction roundToHalfHour(date) {\n const result = new Date(date);\n const minutes = result.getMinutes();\n result.setSeconds(0, 0);\n result.setMinutes(minutes < 30 ? 0 : 30);\n return result;\n}\nfunction pad(value) {\n return String(value).padStart(2, \"0\");\n}\nfunction formatLocalDate(date) {\n return [date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join(\"-\");\n}\nfunction formatLocalTime(date) {\n return [pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds())].join(\":\");\n}\nfunction formatLocalDateTime(date) {\n return `${formatLocalDate(date)}T${formatLocalTime(date)}`;\n}\nfunction formatDateTime(date) {\n if (!date) {\n return \"\";\n }\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 saveCalendarSelection(type, start, end) {\n if (!start || !end) {\n return;\n }\n const isAllDay = type === \"day\" || type === \"days\";\n const selectedDays = isAllDay ? Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)) : null;\n const calendarSelection = {\n type: String(type),\n start: formatLocalDateTime(start),\n end: formatLocalDateTime(end),\n allDay: Boolean(isAllDay),\n selectedDays: selectedDays\n };\n appsmith.updateModel({\n calendarSelection: calendarSelection\n });\n if (type === \"day\") {\n updateSelectionText(`Ausgewählter Tag: ${formatDateTime(start)}`);\n } else if (type === \"days\") {\n updateSelectionText(`Ausgewählte Tage: ${formatDateTime(start)} bis ${formatDateTime(new Date(end.getTime() - 1))}`);\n } else if (type === \"time\") {\n updateSelectionText(`Ausgewählt: ${formatDateTime(start)} bis ${formatDateTime(end)}`);\n }\n if (type === \"day\" || type === \"days\") {\n triggerAppsmithEvent(\"bookingSelected\");\n }\n if (type === \"time\") {\n triggerAppsmithEvent(\"timeSelected\");\n }\n}\nfunction selectSingleMonthDay(date) {\n if (!calendar || !date) {\n return;\n }\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) {\n return;\n }\n calendar.select({\n start: start,\n end: end,\n allDay: true\n });\n }, 0);\n}\nfunction createCalendar() {\n const calendarElement = document.getElementById(\"calendar\");\n if (!calendarElement) {\n return;\n }\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 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 const viewType = info.view.type;\n if (viewType === \"dayGridMonth\") {\n if (isDayNumberClick(info)) {\n openDayView(clickedDate);\n return;\n }\n selectSingleMonthDay(clickedDate);\n return;\n }\n if (viewType === \"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) {\n return isBusinessDayRange(info.start, info.end);\n }\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 selectedDays = Math.round((info.end.getTime() - info.start.getTime()) / (24 * 60 * 60 * 1000));\n saveCalendarSelection(selectedDays === 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 saveCalendarSelection(\"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 allDaySlot: false,\n nowIndicator: true,\n events: getEvents(),\n datesSet(info) {\n updateCalendarViewSettings(info.view.type);\n const element = document.getElementById(\"calendarSelection\");\n if (!element) {\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n element.textContent = \"\";\n element.style.display = \"none\";\n } else {\n element.style.display = \"block\";\n }\n },\n eventClick(info) {\n const event = info.event;\n const start = event.start ? formatDateTime(event.start) : \"\";\n const end = event.end ? formatDateTime(event.end) : \"\";\n const rangeText = end ? `${start} bis ${end}` : start;\n updateSelectionText(`${event.title || \"Buchung\"} ${rangeText}`);\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) {\n return;\n }\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": "{{showModal(Platzbuchung.name);}}",
"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;\n\nconst DAY_HEIGHT = 600;\nconst DAY_CONTENT_HEIGHT = 480;\n\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\n if (typeof model === \"string\") {\n try {\n return JSON.parse(model);\n } catch (error) {\n return {};\n }\n }\n\n return model;\n}\n\nfunction getEvents() {\n const model = getModel();\n return Array.isArray(model.events) ? model.events : [];\n}\n\nfunction getSeatName() {\n const model = getModel();\n return model.seatName || model.seatId || \"Kein Platz ausgewählt\";\n}\n\nfunction updateSeatTitle() {\n const element = document.getElementById(\"calendarSeatTitle\");\n\n if (!element) {\n return;\n }\n\n element.textContent = `Kalender für ${getSeatName()}`;\n}\n\nfunction isDayView() {\n return Boolean(\n calendar &&\n calendar.view &&\n calendar.view.type === \"timeGridDay\"\n );\n}\n\nfunction updateSelectionText(text) {\n const element = document.getElementById(\"calendarSelection\");\n\n if (!element) {\n return;\n }\n\n if (isDayView()) {\n element.textContent = \"\";\n element.style.display = \"none\";\n return;\n }\n\n element.style.display = \"block\";\n element.textContent = text;\n}\n\nfunction updateCalendarViewSettings(viewType) {\n if (!calendar) {\n return;\n }\n\n const dayView = viewType === \"timeGridDay\";\n\n calendar.setOption(\"height\", dayView ? DAY_HEIGHT : MONTH_HEIGHT);\n calendar.setOption(\n \"contentHeight\",\n dayView ? DAY_CONTENT_HEIGHT : MONTH_CONTENT_HEIGHT\n );\n calendar.setOption(\"expandRows\", !dayView);\n\n if (typeof calendar.updateSize === \"function\") {\n calendar.updateSize();\n }\n}\n\nfunction scheduleResize() {\n window.clearTimeout(resizeTimer);\n\n resizeTimer = window.setTimeout(() => {\n if (calendar) {\n calendar.updateSize();\n }\n }, 250);\n}\n\nfunction isWeekend(date) {\n const day = date.getDay();\n return day === 0 || day === 6;\n}\n\nfunction isBusinessDay(date) {\n const day = date.getDay();\n return day >= 1 && day <= 5;\n}\n\nfunction getMinutesOfDay(date) {\n return date.getHours() * 60 + date.getMinutes();\n}\n\nfunction isBusinessTimeRange(start, end) {\n if (!start || !end) {\n return false;\n }\n\n if (!isBusinessDay(start)) {\n return false;\n }\n\n if (start.toDateString() !== end.toDateString()) {\n return false;\n }\n\n const startMinutes = getMinutesOfDay(start);\n const endMinutes = getMinutesOfDay(end);\n\n return (\n startMinutes >= BUSINESS_START_MINUTES &&\n endMinutes <= BUSINESS_END_MINUTES &&\n endMinutes > startMinutes\n );\n}\n\nfunction isBusinessDayRange(start, end) {\n if (!start || !end || end <= start) {\n return false;\n }\n\n const lastSelectedDay = new Date(end.getTime() - 1);\n const current = new Date(start);\n\n current.setHours(12, 0, 0, 0);\n lastSelectedDay.setHours(12, 0, 0, 0);\n\n while (current <= lastSelectedDay) {\n if (!isBusinessDay(current)) {\n return false;\n }\n\n current.setDate(current.getDate() + 1);\n }\n\n return true;\n}\n\nfunction showWeekendMessage() {\n updateSelectionText(\"Samstag und Sonntag sind nicht buchbar.\");\n}\n\nfunction showBusinessHoursMessage() {\n updateSelectionText(\n \"Reservierungen sind Montag bis Freitag von 07:00 bis 18:00 Uhr möglich.\"\n );\n}\n\nfunction openDayView(date) {\n if (!calendar) {\n return;\n }\n\n if (isWeekend(date)) {\n showWeekendMessage();\n return;\n }\n\n calendar.changeView(\"timeGridDay\", date);\n}\n\nfunction isDayNumberClick(info) {\n const target = info.jsEvent && info.jsEvent.target;\n\n if (!target || typeof target.closest !== \"function\") {\n return false;\n }\n\n return Boolean(target.closest(\".fc-daygrid-day-number\"));\n}\n\nfunction roundToHalfHour(date) {\n const result = new Date(date);\n const minutes = result.getMinutes();\n\n result.setSeconds(0, 0);\n result.setMinutes(minutes < 30 ? 0 : 30);\n\n return result;\n}\n\nfunction pad(value) {\n return String(value).padStart(2, \"0\");\n}\n\nfunction formatLocalDate(date) {\n return [\n date.getFullYear(),\n pad(date.getMonth() + 1),\n pad(date.getDate())\n ].join(\"-\");\n}\n\nfunction formatLocalTime(date) {\n return [\n pad(date.getHours()),\n pad(date.getMinutes()),\n pad(date.getSeconds())\n ].join(\":\");\n}\n\nfunction formatLocalDateTime(date) {\n return `${formatLocalDate(date)}T${formatLocalTime(date)}`;\n}\n\nfunction formatDateTime(date) {\n if (!date) {\n return \"\";\n }\n\n return new Intl.DateTimeFormat(\"de-DE\", {\n dateStyle: \"short\",\n timeStyle: \"short\"\n }).format(date);\n}\n\nfunction triggerAppsmithEvent(eventName) {\n try {\n appsmith.triggerEvent(eventName);\n } catch (error) {\n console.warn(\n `Event ${eventName} ist nicht konfiguriert.`,\n error\n );\n }\n}\n\nfunction saveCalendarSelection(type, start, end) {\n if (!start || !end) {\n return;\n }\n\n const isAllDay = type === \"day\" || type === \"days\";\n\n const selectedDays = isAllDay\n ? Math.round(\n (end.getTime() - start.getTime()) /\n (24 * 60 * 60 * 1000)\n )\n : null;\n\n const calendarSelection = {\n type: String(type),\n start: formatLocalDateTime(start),\n end: formatLocalDateTime(end),\n allDay: Boolean(isAllDay),\n selectedDays: selectedDays\n };\n\n appsmith.updateModel({\n calendarSelection: calendarSelection\n });\n\n if (type === \"day\") {\n updateSelectionText(\n `Ausgewählter Tag: ${formatDateTime(start)}`\n );\n } else if (type === \"days\") {\n updateSelectionText(\n `Ausgewählte Tage: ${formatDateTime(start)} bis ${formatDateTime(\n new Date(end.getTime() - 1)\n )}`\n );\n } else if (type === \"time\") {\n updateSelectionText(\n `Ausgewählt: ${formatDateTime(start)} bis ${formatDateTime(end)}`\n );\n }\n\n if (type === \"day\" || type === \"days\") {\n triggerAppsmithEvent(\"bookingSelected\");\n }\n\n if (type === \"time\") {\n triggerAppsmithEvent(\"timeSelected\");\n }\n}\n\nfunction selectSingleMonthDay(date) {\n if (!calendar || !date) {\n return;\n }\n\n if (isWeekend(date)) {\n calendar.unselect();\n showWeekendMessage();\n return;\n }\n\n const start = new Date(date);\n start.setHours(0, 0, 0, 0);\n\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n\n calendar.unselect();\n\n window.setTimeout(() => {\n if (!calendar) {\n return;\n }\n\n calendar.select({\n start: start,\n end: end,\n allDay: true\n });\n }, 0);\n}\n\nfunction createCalendar() {\n const calendarElement = document.getElementById(\"calendar\");\n\n if (!calendarElement) {\n return;\n }\n\n if (typeof FullCalendar === \"undefined\") {\n calendarElement.textContent =\n \"FullCalendar konnte nicht geladen werden.\";\n return;\n }\n\n if (calendar) {\n calendar.destroy();\n calendar = null;\n }\n\n calendar = new FullCalendar.Calendar(calendarElement, {\n initialView: \"dayGridMonth\",\n timeZone: \"local\",\n locale: \"de\",\n firstDay: 1,\n\n businessHours: {\n daysOfWeek: [1, 2, 3, 4, 5],\n startTime: \"07:00\",\n endTime: \"18:00\"\n },\n\n navLinks: true,\n\n navLinkDayClick(date) {\n openDayView(date);\n },\n\n dateClick(info) {\n const clickedDate = info.date;\n const viewType = info.view.type;\n\n if (viewType === \"dayGridMonth\") {\n if (isDayNumberClick(info)) {\n openDayView(clickedDate);\n return;\n }\n\n selectSingleMonthDay(clickedDate);\n return;\n }\n\n if (viewType === \"timeGridDay\") {\n if (isWeekend(clickedDate)) {\n showWeekendMessage();\n return;\n }\n\n const startSlot = roundToHalfHour(clickedDate);\n const endSlot = new Date(\n startSlot.getTime() + SLOT_MINUTES * 60 * 1000\n );\n\n if (!isBusinessTimeRange(startSlot, endSlot)) {\n showBusinessHoursMessage();\n return;\n }\n\n calendar.unselect();\n calendar.select({\n start: startSlot,\n end: endSlot,\n allDay: false\n });\n }\n },\n\n selectable: true,\n selectMirror: false,\n unselectAuto: false,\n selectMinDistance: 5,\n\n selectAllow(info) {\n if (info.allDay) {\n return isBusinessDayRange(info.start, info.end);\n }\n\n return isBusinessTimeRange(info.start, info.end);\n },\n\n editable: false,\n eventStartEditable: false,\n eventDurationEditable: false,\n\n select(info) {\n if (info.view.type === \"dayGridMonth\") {\n const selectedDays = Math.round(\n (info.end.getTime() - info.start.getTime()) /\n (24 * 60 * 60 * 1000)\n );\n\n saveCalendarSelection(\n selectedDays === 1 ? \"day\" : \"days\",\n info.start,\n info.end\n );\n\n return;\n }\n\n if (info.view.type === \"timeGridDay\") {\n if (!isBusinessTimeRange(info.start, info.end)) {\n calendar.unselect();\n showBusinessHoursMessage();\n return;\n }\n\n saveCalendarSelection(\"time\", info.start, info.end);\n }\n },\n\n unselect() {\n const element = document.getElementById(\"calendarSelection\");\n\n if (isDayView()) {\n if (element) {\n element.textContent = \"\";\n element.style.display = \"none\";\n }\n return;\n }\n\n if (element) {\n element.style.display = \"block\";\n element.textContent = \"Keine Auswahl\";\n }\n\n appsmith.updateModel({\n calendarSelection: null\n });\n },\n\n height: MONTH_HEIGHT,\n contentHeight: MONTH_CONTENT_HEIGHT,\n expandRows: true,\n\n headerToolbar: {\n left: \"prev,next today\",\n center: \"title\",\n right: \"dayGridMonth,timeGridDay\"\n },\n\n buttonText: {\n today: \"Heute\",\n month: \"Monat\",\n day: \"Tag\"\n },\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 allDaySlot: false,\n nowIndicator: true,\n events: getEvents(),\n\n datesSet(info) {\n updateCalendarViewSettings(info.view.type);\n\n const element = document.getElementById(\"calendarSelection\");\n\n if (!element) {\n return;\n }\n\n if (info.view.type === \"timeGridDay\") {\n element.textContent = \"\";\n element.style.display = \"none\";\n } else {\n element.style.display = \"block\";\n }\n },\n\n eventClick(info) {\n const event = info.event;\n const start = event.start ? formatDateTime(event.start) : \"\";\n const end = event.end ? formatDateTime(event.end) : \"\";\n const rangeText = end ? `${start} bis ${end}` : start;\n\n updateSelectionText(\n `${event.title || \"Buchung\"} ${rangeText}`\n );\n }\n });\n\n calendar.render();\n updateCalendarViewSettings(calendar.view.type);\n}\n\nfunction refreshCalendarEvents() {\n if (!calendar) {\n createCalendar();\n return;\n }\n\n calendar.removeAllEvents();\n calendar.addEventSource(getEvents());\n calendar.updateSize();\n}\n\nfunction registerResizeHandling() {\n if (resizeRegistered) {\n return;\n }\n\n resizeRegistered = true;\n window.addEventListener(\"resize\", scheduleResize);\n}\n\nappsmith.onReady(() => {\n updateSeatTitle();\n createCalendar();\n registerResizeHandling();\n\n if (!modelChangeRegistered) {\n modelChangeRegistered = true;\n\n appsmith.onModelChange(() => {\n updateSeatTitle();\n refreshCalendarEvents();\n });\n }\n});"
},
"version": 1,
"widgetId": "ad7fob2vs9",
"widgetName": "Custom5"
}