Files
Sitzplatzbuchungstool/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json
T
2026-08-17 14:59:39 +00:00

72 lines
42 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",
"borderColor": "#E0DEDE",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"borderWidth": "1",
"bottomRow": 69,
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"defaultModel": "{{\n {\n events: GetBookings.data || [],\n seatId: appsmith.store.selectedSeatId,\n seatName: appsmith.store.selectedSeatName\n }\n}}",
"dynamicBindingPathList": [
{
"key": "theme"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
},
{
"key": "defaultModel"
}
],
"dynamicHeight": "FIXED",
"dynamicPropertyPathList": [
{
"key": "seatSelected"
}
],
"dynamicTriggerPathList": [
{
"key": "seatSelected"
}
],
"events": [
"seatSelected"
],
"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": 63,
"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(() => GetBookings.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;\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 updateSelectionText(text) {\n const element = document.getElementById(\"calendarSelection\");\n if (!element) {\n return;\n }\n const isDayView = calendar && calendar.view && calendar.view.type === \"timeGridDay\";\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 isDayView = viewType === \"timeGridDay\";\n calendar.setOption(\"height\", isDayView ? DAY_HEIGHT : MONTH_HEIGHT);\n calendar.setOption(\"contentHeight\", isDayView ? DAY_CONTENT_HEIGHT : MONTH_CONTENT_HEIGHT);\n calendar.setOption(\"expandRows\", !isDayView);\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) {\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}\n\n/*\n * Lokales Datum ohne UTC-Verschiebung.\n * Beispiel: 17.08.2026\n */\nfunction formatLocalDate(date) {\n return [date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join(\"-\");\n}\n\n/*\n * Lokale Uhrzeit ohne UTC-Verschiebung.\n * Beispiel: 09:00:00\n */\nfunction formatLocalTime(date) {\n return [pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds())].join(\":\");\n}\n\n/*\n * Lokaler ISO-ähnlicher Wert ohne Z.\n * Beispiel: 2026-08-17T09:00:00\n *\n * Wichtig:\n * Nicht toISOString() verwenden.\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 saveCalendarSelection(type, start, end) {\n const isAllDay = type === \"day\" || type === \"days\";\n const selectedDays = isAllDay ? Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)) : null;\n\n /*\n * Wichtig:\n * Lokale Werte speichern, nicht toISOString().\n */\n const calendarSelection = {\n type: type,\n start: formatLocalDateTime(start),\n end: formatLocalDateTime(end),\n allDay: isAllDay,\n selectedDays: selectedDays\n };\n appsmith.updateModel({\n calendarSelection: calendarSelection\n });\n if (type === \"day\") {\n updateSelectionText(`Ausgewählter Tag: ${formatDateTime(start)}`);\n }\n if (type === \"days\") {\n updateSelectionText(`Ausgewählte Tage: ${formatDateTime(start)} bis ${formatDateTime(new Date(end.getTime() - 1))}`);\n }\n if (type === \"time\") {\n updateSelectionText(`Ausgewählt: ${formatDateTime(start)} bis ${formatDateTime(end)}`);\n }\n if (type === \"day\" || type === \"days\") {\n try {\n appsmith.triggerEvent(\"bookingSelected\", calendarSelection);\n } catch (error) {\n console.warn(\"Event bookingSelected ist nicht konfiguriert.\", error);\n }\n }\n if (type === \"time\") {\n try {\n appsmith.triggerEvent(\"timeSelected\", calendarSelection);\n } catch (error) {\n console.warn(\"Event timeSelected ist nicht konfiguriert.\", error);\n }\n }\n}\nfunction selectSingleMonthDay(date) {\n if (!calendar || !date) {\n return;\n }\n if (isWeekend(date)) {\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 /*\n * Lokale Browser-Zeitzone.\n */\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\n /*\n * info.date wird als lokale\n * Browserzeit verwendet.\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 selectionElement = document.getElementById(\"calendarSelection\");\n const isDayView = calendar && calendar.view && calendar.view.type === \"timeGridDay\";\n if (isDayView) {\n if (selectionElement) {\n selectionElement.textContent = \"\";\n selectionElement.style.display = \"none\";\n }\n return;\n }\n if (selectionElement) {\n selectionElement.style.display = \"block\";\n selectionElement.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 selectionElement = document.getElementById(\"calendarSelection\");\n if (!selectionElement) {\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n selectionElement.textContent = \"\";\n selectionElement.style.display = \"none\";\n } else {\n selectionElement.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 window.addEventListener(\"resize\", scheduleResize);\n}\nappsmith.onReady(() => {\n updateSeatTitle();\n createCalendar();\n registerResizeHandling();\n appsmith.onModelChange(() => {\n updateSeatTitle();\n refreshCalendarEvents();\n });\n});"
},
"theme": "{{appsmith.theme}}",
"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;\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;\n\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\n return Array.isArray(model.events)\n ? model.events\n : [];\n}\n\nfunction getSeatName() {\n const model = getModel();\n\n return (\n model.seatName ||\n model.seatId ||\n \"Kein Platz ausgewählt\"\n );\n}\n\nfunction updateSeatTitle() {\n const element =\n document.getElementById(\n \"calendarSeatTitle\"\n );\n\n if (!element) {\n return;\n }\n\n element.textContent =\n `Kalender für ${getSeatName()}`;\n}\n\nfunction updateSelectionText(text) {\n const element =\n document.getElementById(\n \"calendarSelection\"\n );\n\n if (!element) {\n return;\n }\n\n const isDayView =\n calendar &&\n calendar.view &&\n calendar.view.type ===\n \"timeGridDay\";\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 isDayView =\n viewType === \"timeGridDay\";\n\n calendar.setOption(\n \"height\",\n isDayView\n ? DAY_HEIGHT\n : MONTH_HEIGHT\n );\n\n calendar.setOption(\n \"contentHeight\",\n isDayView\n ? DAY_CONTENT_HEIGHT\n : MONTH_CONTENT_HEIGHT\n );\n\n calendar.setOption(\n \"expandRows\",\n !isDayView\n );\n\n if (\n typeof calendar.updateSize ===\n \"function\"\n ) {\n calendar.updateSize();\n }\n}\n\nfunction scheduleResize() {\n window.clearTimeout(\n resizeTimer\n );\n\n resizeTimer =\n window.setTimeout(() => {\n if (calendar) {\n calendar.updateSize();\n }\n }, 250);\n}\n\nfunction isWeekend(date) {\n const day =\n date.getDay();\n\n return (\n day === 0 ||\n day === 6\n );\n}\n\nfunction isBusinessDay(date) {\n const day =\n date.getDay();\n\n return (\n day >= 1 &&\n day <= 5\n );\n}\n\nfunction getMinutesOfDay(date) {\n return (\n date.getHours() * 60 +\n date.getMinutes()\n );\n}\n\nfunction isBusinessTimeRange(\n start,\n end\n) {\n if (!start || !end) {\n return false;\n }\n\n if (!isBusinessDay(start)) {\n return false;\n }\n\n if (\n start.toDateString() !==\n end.toDateString()\n ) {\n return false;\n }\n\n const startMinutes =\n getMinutesOfDay(start);\n\n const endMinutes =\n getMinutesOfDay(end);\n\n return (\n startMinutes >=\n BUSINESS_START_MINUTES &&\n endMinutes <=\n BUSINESS_END_MINUTES &&\n endMinutes > startMinutes\n );\n}\n\nfunction isBusinessDayRange(\n start,\n end\n) {\n if (!start || !end) {\n return false;\n }\n\n const lastSelectedDay =\n new Date(\n end.getTime() - 1\n );\n\n const current =\n new Date(start);\n\n current.setHours(\n 12,\n 0,\n 0,\n 0\n );\n\n lastSelectedDay.setHours(\n 12,\n 0,\n 0,\n 0\n );\n\n while (\n current <=\n lastSelectedDay\n ) {\n if (!isBusinessDay(current)) {\n return false;\n }\n\n current.setDate(\n current.getDate() + 1\n );\n }\n\n return true;\n}\n\nfunction showWeekendMessage() {\n updateSelectionText(\n \"Samstag und Sonntag sind nicht buchbar.\"\n );\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(\n \"timeGridDay\",\n date\n );\n}\n\nfunction isDayNumberClick(info) {\n const target =\n info.jsEvent &&\n info.jsEvent.target;\n\n if (\n !target ||\n typeof target.closest !==\n \"function\"\n ) {\n return false;\n }\n\n return Boolean(\n target.closest(\n \".fc-daygrid-day-number\"\n )\n );\n}\n\nfunction roundToHalfHour(date) {\n const result =\n new Date(date);\n\n const minutes =\n result.getMinutes();\n\n result.setSeconds(\n 0,\n 0\n );\n\n result.setMinutes(\n minutes < 30\n ? 0\n : 30\n );\n\n return result;\n}\n\nfunction pad(value) {\n return String(value).padStart(\n 2,\n \"0\"\n );\n}\n\n/*\n * Lokales Datum ohne UTC-Verschiebung.\n * Beispiel: 17.08.2026\n */\nfunction formatLocalDate(date) {\n return [\n date.getFullYear(),\n pad(date.getMonth() + 1),\n pad(date.getDate())\n ].join(\"-\");\n}\n\n/*\n * Lokale Uhrzeit ohne UTC-Verschiebung.\n * Beispiel: 09:00:00\n */\nfunction formatLocalTime(date) {\n return [\n pad(date.getHours()),\n pad(date.getMinutes()),\n pad(date.getSeconds())\n ].join(\":\");\n}\n\n/*\n * Lokaler ISO-ähnlicher Wert ohne Z.\n * Beispiel: 2026-08-17T09:00:00\n *\n * Wichtig:\n * Nicht toISOString() verwenden.\n */\nfunction formatLocalDateTime(date) {\n return (\n `${formatLocalDate(date)}T` +\n `${formatLocalTime(date)}`\n );\n}\n\nfunction formatDateTime(date) {\n if (!date) {\n return \"\";\n }\n\n return new Intl.DateTimeFormat(\n \"de-DE\",\n {\n dateStyle: \"short\",\n timeStyle: \"short\"\n }\n ).format(date);\n}\n\nfunction saveCalendarSelection(\n type,\n start,\n end\n) {\n const isAllDay =\n type === \"day\" ||\n type === \"days\";\n\n const selectedDays =\n isAllDay\n ? Math.round(\n (\n end.getTime() -\n start.getTime()\n ) /\n (24 * 60 * 60 * 1000)\n )\n : null;\n\n /*\n * Wichtig:\n * Lokale Werte speichern, nicht toISOString().\n */\n const calendarSelection = {\n type: type,\n start: formatLocalDateTime(start),\n end: formatLocalDateTime(end),\n allDay: isAllDay,\n selectedDays: selectedDays\n };\n\n appsmith.updateModel({\n calendarSelection:\n calendarSelection\n });\n\n if (type === \"day\") {\n updateSelectionText(\n `Ausgewählter Tag: ${\n formatDateTime(start)\n }`\n );\n }\n\n if (type === \"days\") {\n updateSelectionText(\n `Ausgewählte Tage: ${\n formatDateTime(start)\n } bis ${\n formatDateTime(\n new Date(\n end.getTime() - 1\n )\n )\n }`\n );\n }\n\n if (type === \"time\") {\n updateSelectionText(\n `Ausgewählt: ${\n formatDateTime(start)\n } bis ${\n formatDateTime(end)\n }`\n );\n }\n\n if (\n type === \"day\" ||\n type === \"days\"\n ) {\n try {\n appsmith.triggerEvent(\n \"bookingSelected\",\n calendarSelection\n );\n } catch (error) {\n console.warn(\n \"Event bookingSelected ist nicht konfiguriert.\",\n error\n );\n }\n }\n\n if (type === \"time\") {\n try {\n appsmith.triggerEvent(\n \"timeSelected\",\n calendarSelection\n );\n } catch (error) {\n console.warn(\n \"Event timeSelected ist nicht konfiguriert.\",\n error\n );\n }\n }\n}\n\nfunction selectSingleMonthDay(\n date\n) {\n if (!calendar || !date) {\n return;\n }\n\n if (isWeekend(date)) {\n showWeekendMessage();\n return;\n }\n\n const start =\n new Date(date);\n\n start.setHours(\n 0,\n 0,\n 0,\n 0\n );\n\n const end =\n new Date(start);\n\n end.setDate(\n end.getDate() + 1\n );\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 =\n document.getElementById(\n \"calendar\"\n );\n\n if (!calendarElement) {\n return;\n }\n\n if (\n typeof FullCalendar ===\n \"undefined\"\n ) {\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 =\n new FullCalendar.Calendar(\n calendarElement,\n {\n initialView:\n \"dayGridMonth\",\n\n /*\n * Lokale Browser-Zeitzone.\n */\n timeZone: \"local\",\n\n locale: \"de\",\n firstDay: 1,\n\n businessHours: {\n daysOfWeek: [\n 1,\n 2,\n 3,\n 4,\n 5\n ],\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 =\n info.date;\n\n const viewType =\n info.view.type;\n\n if (\n viewType ===\n \"dayGridMonth\"\n ) {\n if (\n isDayNumberClick(info)\n ) {\n openDayView(\n clickedDate\n );\n return;\n }\n\n selectSingleMonthDay(\n clickedDate\n );\n\n return;\n }\n\n if (\n viewType ===\n \"timeGridDay\"\n ) {\n if (\n isWeekend(\n clickedDate\n )\n ) {\n showWeekendMessage();\n return;\n }\n\n /*\n * info.date wird als lokale\n * Browserzeit verwendet.\n */\n const startSlot =\n roundToHalfHour(\n clickedDate\n );\n\n const endSlot =\n new Date(\n startSlot.getTime() +\n SLOT_MINUTES *\n 60 *\n 1000\n );\n\n if (\n !isBusinessTimeRange(\n startSlot,\n endSlot\n )\n ) {\n showBusinessHoursMessage();\n return;\n }\n\n calendar.unselect();\n\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(\n info.start,\n info.end\n );\n }\n\n return isBusinessTimeRange(\n info.start,\n info.end\n );\n },\n\n editable: false,\n eventStartEditable: false,\n eventDurationEditable: false,\n\n select(info) {\n if (\n info.view.type ===\n \"dayGridMonth\"\n ) {\n const selectedDays =\n Math.round(\n (\n info.end.getTime() -\n info.start.getTime()\n ) /\n (24 * 60 * 60 * 1000)\n );\n\n saveCalendarSelection(\n selectedDays === 1\n ? \"day\"\n : \"days\",\n info.start,\n info.end\n );\n\n return;\n }\n\n if (\n info.view.type ===\n \"timeGridDay\"\n ) {\n if (\n !isBusinessTimeRange(\n info.start,\n info.end\n )\n ) {\n calendar.unselect();\n showBusinessHoursMessage();\n return;\n }\n\n saveCalendarSelection(\n \"time\",\n info.start,\n info.end\n );\n }\n },\n\n unselect() {\n const selectionElement =\n document.getElementById(\n \"calendarSelection\"\n );\n\n const isDayView =\n calendar &&\n calendar.view &&\n calendar.view.type ===\n \"timeGridDay\";\n\n if (isDayView) {\n if (selectionElement) {\n selectionElement.textContent =\n \"\";\n\n selectionElement.style.display =\n \"none\";\n }\n\n return;\n }\n\n if (selectionElement) {\n selectionElement.style.display =\n \"block\";\n\n selectionElement.textContent =\n \"Keine Auswahl\";\n }\n\n appsmith.updateModel({\n calendarSelection: null\n });\n },\n\n height: MONTH_HEIGHT,\n contentHeight:\n MONTH_CONTENT_HEIGHT,\n expandRows: true,\n\n headerToolbar: {\n left:\n \"prev,next today\",\n center: \"title\",\n right:\n \"dayGridMonth,timeGridDay\"\n },\n\n buttonText: {\n today: \"Heute\",\n month: \"Monat\",\n day: \"Tag\"\n },\n\n slotDuration:\n \"00:30:00\",\n\n snapDuration:\n \"00:30:00\",\n\n slotMinTime:\n \"07:00:00\",\n\n slotMaxTime:\n \"22:00:00\",\n\n scrollTime:\n \"08:00:00\",\n\n scrollTimeReset: false,\n\n allDaySlot: false,\n\n nowIndicator: true,\n\n events: getEvents(),\n\n datesSet(info) {\n updateCalendarViewSettings(\n info.view.type\n );\n\n const selectionElement =\n document.getElementById(\n \"calendarSelection\"\n );\n\n if (\n !selectionElement\n ) {\n return;\n }\n\n if (\n info.view.type ===\n \"timeGridDay\"\n ) {\n selectionElement.textContent =\n \"\";\n\n selectionElement.style.display =\n \"none\";\n } else {\n selectionElement.style.display =\n \"block\";\n }\n },\n\n eventClick(info) {\n const event =\n info.event;\n\n const start =\n event.start\n ? formatDateTime(\n event.start\n )\n : \"\";\n\n const end =\n event.end\n ? formatDateTime(\n event.end\n )\n : \"\";\n\n const rangeText =\n end\n ? `${start} bis ${end}`\n : start;\n\n updateSelectionText(\n `${\n event.title ||\n \"Buchung\"\n } ${rangeText}`\n );\n }\n }\n );\n\n calendar.render();\n\n updateCalendarViewSettings(\n calendar.view.type\n );\n}\n\nfunction refreshCalendarEvents() {\n if (!calendar) {\n createCalendar();\n return;\n }\n\n calendar.removeAllEvents();\n calendar.addEventSource(\n getEvents()\n );\n calendar.updateSize();\n}\n\nfunction registerResizeHandling() {\n window.addEventListener(\n \"resize\",\n scheduleResize\n );\n}\n\nappsmith.onReady(() => {\n updateSeatTitle();\n createCalendar();\n registerResizeHandling();\n\n appsmith.onModelChange(() => {\n updateSeatTitle();\n refreshCalendarEvents();\n });\n});"
},
"version": 1,
"widgetId": "ad7fob2vs9",
"widgetName": "Custom5"
}