diff --git a/pages/Sitzplätze/queries/DeleteBooking/DeleteBooking.txt b/pages/Sitzplätze/queries/DeleteBooking/DeleteBooking.txt new file mode 100644 index 0000000..f2411f9 --- /dev/null +++ b/pages/Sitzplätze/queries/DeleteBooking/DeleteBooking.txt @@ -0,0 +1,5 @@ +DELETE FROM `buchungen` +WHERE id = + {{ this.params.bookingId }} + AND gebucht_von = + {{ this.params.bookedBy }}; \ No newline at end of file diff --git a/pages/Sitzplätze/queries/DeleteBooking/metadata.json b/pages/Sitzplätze/queries/DeleteBooking/metadata.json new file mode 100644 index 0000000..f6cd67e --- /dev/null +++ b/pages/Sitzplätze/queries/DeleteBooking/metadata.json @@ -0,0 +1,35 @@ +{ + "gitSyncId": "6a6b5b477ebc9edeca96e2f8_766cb29a-c15e-4c36-a828-dda8c3fb28eb", + "id": "Sitzplätze_DeleteBooking", + "pluginId": "mysql-plugin", + "pluginType": "DB", + "unpublishedAction": { + "actionConfiguration": { + "body": "DELETE FROM `buchungen`\nWHERE id =\n {{ this.params.bookingId }}\n AND gebucht_von =\n {{ this.params.bookedBy }};", + "encodeParamsToggle": true, + "paginationType": "NONE", + "pluginSpecifiedTemplates": [ + { + "value": true + } + ], + "timeoutInMillisecond": 10000 + }, + "confirmBeforeExecute": false, + "datasource": { + "id": "MySQL Bürobuchungen", + "isAutoGenerated": false, + "name": "MySQL Bürobuchungen", + "pluginId": "mysql-plugin" + }, + "dynamicBindingPathList": [ + { + "key": "body" + } + ], + "name": "DeleteBooking", + "pageId": "Sitzplätze", + "runBehaviour": "MANUAL", + "userSetOnLoad": false + } +} \ No newline at end of file diff --git a/pages/Sitzplätze/queries/Query1/Query1.txt b/pages/Sitzplätze/queries/Query2/Query2.txt similarity index 100% rename from pages/Sitzplätze/queries/Query1/Query1.txt rename to pages/Sitzplätze/queries/Query2/Query2.txt diff --git a/pages/Sitzplätze/queries/Query1/metadata.json b/pages/Sitzplätze/queries/Query2/metadata.json similarity index 83% rename from pages/Sitzplätze/queries/Query1/metadata.json rename to pages/Sitzplätze/queries/Query2/metadata.json index 8a72b23..ae57523 100644 --- a/pages/Sitzplätze/queries/Query1/metadata.json +++ b/pages/Sitzplätze/queries/Query2/metadata.json @@ -1,6 +1,6 @@ { - "gitSyncId": "6a6b5b477ebc9edeca96e2f8_766cb29a-c15e-4c36-a828-dda8c3fb28eb", - "id": "Sitzplätze_Query1", + "gitSyncId": "6a6b5b477ebc9edeca96e2f8_cdc0389e-ec18-4013-bdb2-a8cefae53fda", + "id": "Sitzplätze_Query2", "pluginId": "mysql-plugin", "pluginType": "DB", "unpublishedAction": { @@ -22,7 +22,7 @@ "name": "MySQL Bürobuchungen", "pluginId": "mysql-plugin" }, - "name": "Query1", + "name": "Query2", "pageId": "Sitzplätze", "runBehaviour": "MANUAL", "userSetOnLoad": false diff --git a/pages/Sitzplätze/widgets/Custom4.json b/pages/Sitzplätze/widgets/Custom4.json index 88070aa..06699a1 100644 --- a/pages/Sitzplätze/widgets/Custom4.json +++ b/pages/Sitzplätze/widgets/Custom4.json @@ -55,7 +55,7 @@ "parentRowSpace": 10, "renderMode": "CANVAS", "rightColumn": 64, - "seatSelected": "{{\n showModal(\n \"SeatCalendar\"\n )\n}}", + "seatSelected": "{{\n storeValue(\n \"selectedSeatId\",\n Custom4.model.seatId ||\n Custom4.model.selectedSeat?.id ||\n \"\",\n false\n )\n .then(() =>\n storeValue(\n \"selectedSeatName\",\n Custom4.model.seatName ||\n Custom4.model.selectedSeat?.name ||\n \"\",\n false\n )\n )\n .then(() =>\n GetSeatBookings.run()\n )\n .then(() =>\n showModal(\n SeatCalendar.name\n )\n )\n}}", "srcDoc": { "css": "@charset \"UTF-8\";\nhtml,\nbody {\n margin: 0;\n width: 100%;\n height: 100%;\n overflow: auto;\n font-family: Arial, sans-serif;\n}\n\n/* Bild + Overlay */\n#imgHolder {\n position: relative;\n width: 100%;\n height: 700px; /* feste Höhe, damit sicher nicht 0 */\n overflow: hidden;\n border-radius: 6px;\n background: #f3f4f6;\n}\n\n#floorplanImage {\n position: absolute;\n inset: 0;\n z-index: 1;\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n#seatLayer {\n position: absolute;\n inset: 0;\n z-index: 2;\n width: 100%;\n height: 100%;\n pointer-events: none; /* Punkte bekommen später pointer-events:auto */\n}\n\n/* Sitzpunkte */\n.seat {\n position: absolute;\n z-index: 3;\n display: block;\n width: 18px;\n height: 18px;\n padding: 0;\n transform: translate(-50%, -50%);\n border: 2px solid #ffffff;\n border-radius: 50%;\n background: #22c55e;\n color: #22c55e;\n cursor: pointer;\n pointer-events: auto;\n appearance: none;\n box-sizing: border-box;\n box-shadow: 0 0 5px currentColor, 0 0 10px currentColor;\n transition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n\n.seat:hover {\n z-index: 10;\n transform: translate(-50%, -50%) scale(1.45);\n box-shadow: 0 0 8px currentColor, 0 0 16px currentColor;\n}\n\n/* Farben nach Status */\n.seat.frei {\n background: #22c55e; /* Grün */\n color: #22c55e;\n}\n\n.seat.belegt {\n background: #ef4444; /* Rot */\n color: #ef4444;\n}\n\n.seat.selected {\n outline: 3px solid #2563eb;\n outline-offset: 3px;\n}\n\n/* Legende */\n#legend {\n display: flex;\n flex-wrap: wrap;\n gap: 14px;\n margin-top: 8px;\n color: #374151;\n font-size: 13px;\n}\n\n.legendItem {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n}\n\n.legendColor {\n display: inline-block;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n}\n\n.legendColor.frei {\n background: #22c55e;\n}\n\n.legendColor.belegt {\n background: #ef4444;\n}", "html": "
\n\n
\n \n \"Büroplan\"\n\t\t
\n\t\t\n\t\t\n
\n
", diff --git a/pages/Sitzplätze/widgets/MyBookingDetailsModal/DeleteBookingButton.json b/pages/Sitzplätze/widgets/MyBookingDetailsModal/DeleteBookingButton.json new file mode 100644 index 0000000..655236a --- /dev/null +++ b/pages/Sitzplätze/widgets/MyBookingDetailsModal/DeleteBookingButton.json @@ -0,0 +1,56 @@ +{ + "animateLoading": true, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "bottomRow": 24, + "boxShadow": "none", + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "buttonStyle": "PRIMARY_BUTTON", + "buttonVariant": "PRIMARY", + "disabledWhenInvalid": false, + "dynamicBindingPathList": [ + { + "key": "buttonColor" + }, + { + "key": "borderRadius" + } + ], + "dynamicPropertyPathList": [ + { + "key": "onClick" + } + ], + "dynamicTriggerPathList": [ + { + "key": "onClick" + } + ], + "isDefaultClickDisabled": true, + "isDisabled": false, + "isLoading": false, + "isVisible": true, + "key": "zb1cmuhe3l", + "leftColumn": 41, + "minWidth": 120, + "mobileBottomRow": 22, + "mobileLeftColumn": 47, + "mobileRightColumn": 63, + "mobileTopRow": 18, + "needsErrorInfo": false, + "onClick": "{{\n DeleteBooking\n .run({\n bookingId:\n appsmith.store.selectedBooking?.id,\n\n bookedBy:\n appsmith.user.email ||\n appsmith.user.username\n })\n .then((result) => {\n console.log(\n \"DeleteBooking Ergebnis:\",\n result\n );\n\n return GetSeatBookings.run();\n })\n .then(() => {\n return GetSeats.run();\n })\n .then(() => {\n return closeModal(\n MyBookingDetailsModal.name\n );\n })\n .then(() => {\n return showAlert(\n \"Die Buchung wurde gelöscht.\",\n \"success\"\n );\n })\n .catch((error) => {\n console.error(\n \"Löschen fehlgeschlagen:\",\n error\n );\n\n showAlert(\n \"Die Buchung konnte nicht gelöscht werden.\",\n \"error\"\n );\n });\n}}", + "originalBottomRow": 24, + "originalTopRow": 20, + "parentId": "7731jpp9wk", + "placement": "CENTER", + "recaptchaType": "V3", + "renderMode": "CANVAS", + "resetFormOnClick": false, + "responsiveBehavior": "hug", + "rightColumn": 63, + "text": "Buchung löschen", + "topRow": 20, + "type": "BUTTON_WIDGET", + "version": 1, + "widgetId": "zf6zh6z0sk", + "widgetName": "DeleteBookingButton" +} \ No newline at end of file diff --git a/pages/Sitzplätze/widgets/MyBookingDetailsModal/IconButton2.json b/pages/Sitzplätze/widgets/MyBookingDetailsModal/IconButton2.json new file mode 100644 index 0000000..f0657d0 --- /dev/null +++ b/pages/Sitzplätze/widgets/MyBookingDetailsModal/IconButton2.json @@ -0,0 +1,44 @@ +{ + "animateLoading": true, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "bottomRow": 4, + "boxShadow": "none", + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "buttonVariant": "TERTIARY", + "dynamicBindingPathList": [ + { + "key": "buttonColor" + }, + { + "key": "borderRadius" + } + ], + "dynamicTriggerPathList": [ + { + "key": "onClick" + } + ], + "iconName": "cross", + "iconSize": 24, + "isDisabled": false, + "isLoading": false, + "isVisible": true, + "key": "zaccq9na42", + "leftColumn": 58, + "minWidth": 50, + "mobileBottomRow": 4, + "mobileLeftColumn": 58, + "mobileRightColumn": 64, + "mobileTopRow": 0, + "needsErrorInfo": false, + "onClick": "{{closeModal(MyBookingDetailsModal.name);}}", + "parentId": "7731jpp9wk", + "renderMode": "CANVAS", + "responsiveBehavior": "hug", + "rightColumn": 64, + "topRow": 0, + "type": "ICON_BUTTON_WIDGET", + "version": 1, + "widgetId": "pcmtvkn5jw", + "widgetName": "IconButton2" +} \ No newline at end of file diff --git a/pages/Sitzplätze/widgets/MyBookingDetailsModal/MyBookingDetailsModal.json b/pages/Sitzplätze/widgets/MyBookingDetailsModal/MyBookingDetailsModal.json new file mode 100644 index 0000000..e575a20 --- /dev/null +++ b/pages/Sitzplätze/widgets/MyBookingDetailsModal/MyBookingDetailsModal.json @@ -0,0 +1,85 @@ +{ + "animateLoading": true, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "bottomRow": 289, + "boxShadow": "none", + "canEscapeKeyClose": true, + "canOutsideClickClose": true, + "children": [ + { + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "bottomRow": 260, + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "canExtend": true, + "detachFromLayout": true, + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + } + ], + "dynamicHeight": "AUTO_HEIGHT", + "flexLayers": [], + "isDisabled": false, + "isLoading": false, + "isVisible": true, + "key": "iy86p0btqs", + "leftColumn": 0, + "maxDynamicHeight": 9000, + "minDynamicHeight": 4, + "minHeight": 260, + "minWidth": 450, + "mobileBottomRow": 240, + "mobileLeftColumn": 0, + "mobileRightColumn": 435.75, + "mobileTopRow": 0, + "needsErrorInfo": false, + "parentColumnSpace": 1, + "parentId": "m9c4kqrhxt", + "parentRowSpace": 1, + "renderMode": "CANVAS", + "responsiveBehavior": "fill", + "rightColumn": 435.75, + "shouldScrollContents": false, + "topRow": 0, + "type": "CANVAS_WIDGET", + "version": 1, + "widgetId": "7731jpp9wk", + "widgetName": "Canvas9" + } + ], + "detachFromLayout": true, + "dynamicBindingPathList": [ + { + "key": "borderRadius" + } + ], + "dynamicHeight": "AUTO_HEIGHT", + "height": 260, + "isCanvas": true, + "isLoading": false, + "key": "jc8yhgzuwd", + "leftColumn": 20, + "maxDynamicHeight": 9000, + "minDynamicHeight": 24, + "minHeight": 260, + "mobileBottomRow": 53, + "mobileLeftColumn": 20, + "mobileRightColumn": 44, + "mobileTopRow": 29, + "needsErrorInfo": false, + "parentColumnSpace": 18.15625, + "parentId": "0", + "parentRowSpace": 10, + "renderMode": "CANVAS", + "rightColumn": 44, + "shouldScrollContents": true, + "topRow": 29, + "type": "MODAL_WIDGET", + "version": 2, + "widgetId": "m9c4kqrhxt", + "widgetName": "MyBookingDetailsModal", + "width": 732 +} \ No newline at end of file diff --git a/pages/Sitzplätze/widgets/Platzbuchung/Text4.json b/pages/Sitzplätze/widgets/MyBookingDetailsModal/Text6.json similarity index 62% rename from pages/Sitzplätze/widgets/Platzbuchung/Text4.json rename to pages/Sitzplätze/widgets/MyBookingDetailsModal/Text6.json index edf9ac9..df19a20 100644 --- a/pages/Sitzplätze/widgets/Platzbuchung/Text4.json +++ b/pages/Sitzplätze/widgets/MyBookingDetailsModal/Text6.json @@ -1,7 +1,7 @@ { "animateLoading": true, "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "bottomRow": 92, + "bottomRow": 24, "dynamicBindingPathList": [ { "key": "truncateButtonColor" @@ -19,37 +19,35 @@ "dynamicHeight": "AUTO_HEIGHT", "dynamicTriggerPathList": [], "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", - "fontSize": "1rem", + "fontSize": "1.25rem", "fontStyle": "BOLD", "isLoading": false, "isVisible": true, - "key": "wb75ugacfs", - "leftColumn": 39, + "key": "wnu3ozrv06", + "leftColumn": 1, "maxDynamicHeight": 9000, "minDynamicHeight": 4, "minWidth": 450, - "mobileBottomRow": 56, - "mobileLeftColumn": 43, - "mobileRightColumn": 59, - "mobileTopRow": 52, + "mobileBottomRow": 5, + "mobileLeftColumn": 1, + "mobileRightColumn": 41, + "mobileTopRow": 1, "needsErrorInfo": false, - "originalBottomRow": 92, - "originalTopRow": 43, + "originalBottomRow": 24, + "originalTopRow": 1, "overflow": "NONE", - "parentColumnSpace": 18.56015625, - "parentId": "q44lz1f0e4", - "parentRowSpace": 10, + "parentId": "7731jpp9wk", "renderMode": "CANVAS", "responsiveBehavior": "fill", - "rightColumn": 48, + "rightColumn": 41, "shouldTruncate": false, - "text": "{{\n JSON.stringify(\n appsmith.store.bookingFormData || {},\n null,\n 2\n )\n}}", + "text": "{{\n JSON.stringify(\n appsmith.store.selectedBooking || {},\n null,\n 2\n )\n}}", "textAlign": "LEFT", "textColor": "#231F20", - "topRow": 43, + "topRow": 1, "truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}", "type": "TEXT_WIDGET", "version": 1, - "widgetId": "y723ubcx1i", - "widgetName": "Text4" + "widgetId": "uby200a2fb", + "widgetName": "Text6" } \ No newline at end of file diff --git a/pages/Sitzplätze/widgets/Platzbuchung/BookingEndDate.json b/pages/Sitzplätze/widgets/Platzbuchung/BookingEndDate.json index 0b62464..2305e6d 100644 --- a/pages/Sitzplätze/widgets/Platzbuchung/BookingEndDate.json +++ b/pages/Sitzplätze/widgets/Platzbuchung/BookingEndDate.json @@ -7,17 +7,32 @@ "closeOnSelection": true, "dateFormat": "D MMMM, YYYY", "datePickerType": "DATE_PICKER", - "defaultDate": "2026-08-17T09:59:18.715Z", + "defaultDate": "{{\n appsmith.store.bookingFormData?.endDate ||\n appsmith.store.bookingFormData?.startDate ||\n appsmith.store.calendarSelection?.start?.slice(0, 10) ||\n \"\"\n}}", "dynamicBindingPathList": [ { "key": "accentColor" }, { "key": "borderRadius" + }, + { + "key": "defaultDate" } ], "dynamicHeight": "FIXED", - "dynamicTriggerPathList": [], + "dynamicPropertyPathList": [ + { + "key": "defaultDate" + }, + { + "key": "onDateSelected" + } + ], + "dynamicTriggerPathList": [ + { + "key": "onDateSelected" + } + ], "firstDayOfWeek": 0, "isDisabled": false, "isLoading": false, @@ -40,6 +55,7 @@ "mobileRightColumn": 21, "mobileTopRow": 11, "needsErrorInfo": false, + "onDateSelected": "{{\n storeValue(\n \"bookingFormData\",\n {\n ...(appsmith.store.bookingFormData || {}),\n\n endDate:\n BookingEndDate.selectedDate\n },\n false\n )\n}}", "parentColumnSpace": 8.875, "parentId": "q44lz1f0e4", "parentRowSpace": 10, diff --git a/pages/Sitzplätze/widgets/Platzbuchung/BookingStartDate.json b/pages/Sitzplätze/widgets/Platzbuchung/BookingStartDate.json index 4c46d1f..7c9a763 100644 --- a/pages/Sitzplätze/widgets/Platzbuchung/BookingStartDate.json +++ b/pages/Sitzplätze/widgets/Platzbuchung/BookingStartDate.json @@ -7,17 +7,32 @@ "closeOnSelection": true, "dateFormat": "D MMMM, YYYY", "datePickerType": "DATE_PICKER", - "defaultDate": "2026-08-17T09:59:18.715Z", + "defaultDate": "{{\n appsmith.store.bookingFormData?.startDate ||\n appsmith.store.calendarSelection?.start?.slice(0, 10) ||\n \"\"\n}}", "dynamicBindingPathList": [ { "key": "accentColor" }, { "key": "borderRadius" + }, + { + "key": "defaultDate" } ], "dynamicHeight": "FIXED", - "dynamicTriggerPathList": [], + "dynamicPropertyPathList": [ + { + "key": "defaultDate" + }, + { + "key": "onDateSelected" + } + ], + "dynamicTriggerPathList": [ + { + "key": "onDateSelected" + } + ], "firstDayOfWeek": 0, "isDisabled": false, "isLoading": false, @@ -40,6 +55,7 @@ "mobileRightColumn": 21, "mobileTopRow": 11, "needsErrorInfo": false, + "onDateSelected": "{{\n storeValue(\n \"bookingFormData\",\n {\n ...(appsmith.store.bookingFormData || {}),\n\n startDate:\n BookingStartDate.selectedDate,\n\n endDate:\n BookingStartDate.selectedDate\n },\n false\n )\n}}", "parentColumnSpace": 8.875, "parentId": "q44lz1f0e4", "parentRowSpace": 10, diff --git a/pages/Sitzplätze/widgets/Platzbuchung/ConfirmBookingButton.json b/pages/Sitzplätze/widgets/Platzbuchung/ConfirmBookingButton.json index 66c2eb7..01d46a1 100644 --- a/pages/Sitzplätze/widgets/Platzbuchung/ConfirmBookingButton.json +++ b/pages/Sitzplätze/widgets/Platzbuchung/ConfirmBookingButton.json @@ -1,7 +1,7 @@ { "animateLoading": true, "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "bottomRow": 79, + "bottomRow": 64, "boxShadow": "none", "buttonColor": "{{appsmith.theme.colors.primaryColor}}", "buttonStyle": "PRIMARY_BUTTON", @@ -34,8 +34,8 @@ "mobileTopRow": 18, "needsErrorInfo": false, "onClick": "{{RecurringBooking.prepareRecurringBooking();}}", - "originalBottomRow": 78, - "originalTopRow": 74, + "originalBottomRow": 79, + "originalTopRow": 75, "parentId": "q44lz1f0e4", "placement": "CENTER", "recaptchaType": "V3", @@ -44,7 +44,7 @@ "responsiveBehavior": "hug", "rightColumn": 62, "text": "Buchen", - "topRow": 75, + "topRow": 60, "type": "BUTTON_WIDGET", "version": 1, "widgetId": "iutdgcd629", diff --git a/pages/Sitzplätze/widgets/Platzbuchung/Platzbuchung.json b/pages/Sitzplätze/widgets/Platzbuchung/Platzbuchung.json index df6aa64..a448fbd 100644 --- a/pages/Sitzplätze/widgets/Platzbuchung/Platzbuchung.json +++ b/pages/Sitzplätze/widgets/Platzbuchung/Platzbuchung.json @@ -1,14 +1,14 @@ { "animateLoading": true, "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "bottomRow": 94, + "bottomRow": 66, "boxShadow": "none", "canEscapeKeyClose": true, "canOutsideClickClose": true, "children": [ { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "bottomRow": 940, + "bottomRow": 660, "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", "canExtend": true, "detachFromLayout": true, @@ -58,14 +58,14 @@ ], "dynamicHeight": "AUTO_HEIGHT", "dynamicTriggerPathList": [], - "height": 940, + "height": 660, "isCanvas": true, "isLoading": false, "key": "tfld9k9m85", "leftColumn": 19, "maxDynamicHeight": 9000, "minDynamicHeight": 10, - "minHeight": 940, + "minHeight": 660, "mobileBottomRow": 39, "mobileLeftColumn": 19, "mobileRightColumn": 43, diff --git a/pages/Sitzplätze/widgets/Platzbuchung/Text5.json b/pages/Sitzplätze/widgets/Platzbuchung/Text5.json deleted file mode 100644 index dda5c93..0000000 --- a/pages/Sitzplätze/widgets/Platzbuchung/Text5.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "animateLoading": true, - "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "bottomRow": 48, - "dynamicBindingPathList": [ - { - "key": "truncateButtonColor" - }, - { - "key": "fontFamily" - }, - { - "key": "borderRadius" - }, - { - "key": "text" - } - ], - "dynamicHeight": "AUTO_HEIGHT", - "dynamicTriggerPathList": [], - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", - "fontSize": "1rem", - "fontStyle": "BOLD", - "isLoading": false, - "isVisible": true, - "key": "wb75ugacfs", - "leftColumn": 48, - "maxDynamicHeight": 9000, - "minDynamicHeight": 4, - "minWidth": 450, - "mobileBottomRow": 47, - "mobileLeftColumn": 48, - "mobileRightColumn": 64, - "mobileTopRow": 43, - "needsErrorInfo": false, - "originalBottomRow": 47, - "originalTopRow": 43, - "overflow": "NONE", - "parentColumnSpace": 18.56015625, - "parentId": "q44lz1f0e4", - "parentRowSpace": 10, - "renderMode": "CANVAS", - "responsiveBehavior": "fill", - "rightColumn": 64, - "shouldTruncate": false, - "text": "{{\n \"Start: \" +\n appsmith.store.bookingFormData?.startDate +\n \" | Ende: \" +\n appsmith.store.bookingFormData?.endDate\n}}", - "textAlign": "LEFT", - "textColor": "#231F20", - "topRow": 43, - "truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}", - "type": "TEXT_WIDGET", - "version": 1, - "widgetId": "8r5ranska0", - "widgetName": "Text5" -} \ No newline at end of file diff --git a/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json b/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json index 6f84418..8fb56ce 100644 --- a/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json +++ b/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json @@ -1,5 +1,6 @@ { "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}}", @@ -28,6 +29,9 @@ }, { "key": "bookingSelected" + }, + { + "key": "bookingClicked" } ], "dynamicTriggerPathList": [ @@ -39,12 +43,16 @@ }, { "key": "timeSelected" + }, + { + "key": "bookingClicked" } ], "events": [ "seatSelected", "bookingSelected", - "timeSelected" + "timeSelected", + "bookingClicked" ], "isCanvas": false, "isLoading": false, @@ -68,7 +76,7 @@ "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": "
\n
\n
\n Kalender\n
\n
\n\n
\n\n
\n Keine Auswahl\n
\n
\n\n\n\n\n", - "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});" + "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 getCurrentUser() {\n const model = getModel();\n return String(model.currentUser || \"\").trim().toLowerCase();\n}\nfunction updateSeatTitle() {\n const element = document.getElementById(\"calendarSeatTitle\");\n if (!element) return;\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) 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\") {\n calendar.updateSize();\n }\n}\nfunction scheduleResize() {\n window.clearTimeout(resizeTimer);\n resizeTimer = window.setTimeout(() => {\n if (calendar) calendar.updateSize();\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) return false;\n if (!isBusinessDay(start)) return false;\n if (start.toDateString() !== end.toDateString()) return false;\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) return false;\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)) return false;\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) 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 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) 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: event.id || extended.id || null,\n platz_id: event.platz_id || extended.platz_id || null,\n title: event.title || extended.title || \"Platzbuchung\",\n start: event.start ? formatLocalDateTime(event.start) : event.start || extended.start || null,\n end: event.end ? formatLocalDateTime(event.end) : event.end || extended.end || null,\n gebucht_von: event.gebucht_von || extended.gebucht_von || null\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} – ${formatDateTime(info.event.start)}`);\n triggerAppsmithEvent(\"bookingClicked\");\n}\nfunction saveCalendarSelection(type, start, end) {\n if (!start || !end) return;\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\n };\n appsmith.updateModel({\n 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) 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) return;\n 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 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 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)) {\n info.el.title = \"Eigene Buchung anklicken, um Details zu öffnen\";\n }\n },\n datesSet(info) {\n updateCalendarViewSettings(info.view.type);\n const element = document.getElementById(\"calendarSelection\");\n if (!element) return;\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 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": "{{showModal(Platzbuchung.name);}}", @@ -77,7 +85,7 @@ "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": "
\n
\n
\n Kalender\n
\n
\n\n
\n\n
\n Keine Auswahl\n
\n
\n\n\n\n\n", - "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});" + "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\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)\n ? model.events\n : [];\n}\n\nfunction getSeatName() {\n const model = getModel();\n return model.seatName || model.seatId || \"Kein Platz ausgewählt\";\n}\n\nfunction getCurrentUser() {\n const model = getModel();\n return String(model.currentUser || \"\")\n .trim()\n .toLowerCase();\n}\n\nfunction updateSeatTitle() {\n const element = document.getElementById(\"calendarSeatTitle\");\n\n if (!element) return;\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) return;\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) return;\n\n const dayView = viewType === \"timeGridDay\";\n\n calendar.setOption(\n \"height\",\n dayView ? DAY_HEIGHT : MONTH_HEIGHT\n );\n\n calendar.setOption(\n \"contentHeight\",\n dayView ? DAY_CONTENT_HEIGHT : MONTH_CONTENT_HEIGHT\n );\n\n calendar.setOption(\n \"expandRows\",\n !dayView\n );\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) calendar.updateSize();\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) return false;\n if (!isBusinessDay(start)) return false;\n if (start.toDateString() !== end.toDateString()) return false;\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) return false;\n\n const lastSelectedDay = new Date(\n end.getTime() - 1\n );\n\n const current = new Date(start);\n current.setHours(12, 0, 0, 0);\n lastSelectedDay.setHours(12, 0, 0, 0);\n\n while (current <= lastSelectedDay) {\n if (!isBusinessDay(current)) return false;\n current.setDate(current.getDate() + 1);\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) return;\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(\n target.closest(\".fc-daygrid-day-number\")\n );\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) return \"\";\n\n return new Intl.DateTimeFormat(\n \"de-DE\",\n {\n dateStyle: \"short\",\n timeStyle: \"short\"\n }\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 normalizeBookingEvent(event) {\n const extended = event.extendedProps || {};\n\n return {\n id:\n event.id ||\n extended.id ||\n null,\n\n platz_id:\n event.platz_id ||\n extended.platz_id ||\n null,\n\n title:\n event.title ||\n extended.title ||\n \"Platzbuchung\",\n\n start:\n event.start\n ? formatLocalDateTime(event.start)\n : event.start ||\n extended.start ||\n null,\n\n end:\n event.end\n ? formatLocalDateTime(event.end)\n : event.end ||\n extended.end ||\n null,\n\n gebucht_von:\n event.gebucht_von ||\n extended.gebucht_von ||\n null\n };\n}\n\nfunction isOwnBooking(booking) {\n const currentUser = getCurrentUser();\n const bookingUser = String(\n booking.gebucht_von || \"\"\n )\n .trim()\n .toLowerCase();\n\n return Boolean(\n currentUser &&\n bookingUser &&\n currentUser === bookingUser\n );\n}\n\nfunction handleBookingClick(info) {\n const booking = normalizeBookingEvent(\n info.event\n );\n\n if (!booking.id) {\n updateSelectionText(\n \"Diese Buchung besitzt keine ID und kann nicht gelöscht werden.\"\n );\n\n return;\n }\n\n if (!isOwnBooking(booking)) {\n triggerAppsmithEvent(\n \"foreignBookingClicked\"\n );\n\n updateSelectionText(\n \"Diese Buchung gehört einem anderen Benutzer.\"\n );\n\n return;\n }\n\n appsmith.updateModel({\n selectedBooking: booking\n });\n\n updateSelectionText(\n `${booking.title} – ${formatDateTime(info.event.start)}`\n );\n\n triggerAppsmithEvent(\n \"bookingClicked\"\n );\n}\n\nfunction saveCalendarSelection(type, start, end) {\n if (!start || !end) return;\n\n const isAllDay =\n type === \"day\" ||\n 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\n };\n\n appsmith.updateModel({\n 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(new Date(end.getTime() - 1))}`\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(\n \"bookingSelected\"\n );\n }\n\n if (type === \"time\") {\n triggerAppsmithEvent(\n \"timeSelected\"\n );\n }\n}\n\nfunction selectSingleMonthDay(date) {\n if (!calendar || !date) return;\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) return;\n\n calendar.select({\n start,\n end,\n allDay: true\n });\n }, 0);\n}\n\nfunction createCalendar() {\n const calendarElement =\n document.getElementById(\"calendar\");\n\n if (!calendarElement) return;\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(\n calendarElement,\n {\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 =\n roundToHalfHour(clickedDate);\n\n const endSlot = new Date(\n startSlot.getTime() +\n SLOT_MINUTES * 60 * 1000\n );\n\n if (\n !isBusinessTimeRange(\n startSlot,\n endSlot\n )\n ) {\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(\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 (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\n ? \"day\"\n : \"days\",\n info.start,\n info.end\n );\n\n return;\n }\n\n if (info.view.type === \"timeGridDay\") {\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 element =\n document.getElementById(\n \"calendarSelection\"\n );\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 =\n \"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\n events: getEvents(),\n\n eventClick(info) {\n handleBookingClick(info);\n },\n\n eventDidMount(info) {\n const booking =\n normalizeBookingEvent(\n info.event\n );\n\n info.el.style.cursor =\n booking.id && isOwnBooking(booking)\n ? \"pointer\"\n : \"default\";\n\n if (\n booking.id &&\n isOwnBooking(booking)\n ) {\n info.el.title =\n \"Eigene Buchung anklicken, um Details zu öffnen\";\n }\n },\n\n datesSet(info) {\n updateCalendarViewSettings(\n info.view.type\n );\n\n const element =\n document.getElementById(\n \"calendarSelection\"\n );\n\n if (!element) return;\n\n if (\n info.view.type === \"timeGridDay\"\n ) {\n element.textContent = \"\";\n element.style.display = \"none\";\n } else {\n element.style.display = \"block\";\n }\n }\n }\n );\n\n calendar.render();\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(getEvents());\n calendar.updateSize();\n}\n\nfunction registerResizeHandling() {\n if (resizeRegistered) return;\n\n resizeRegistered = true;\n window.addEventListener(\n \"resize\",\n scheduleResize\n );\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",