diff --git a/pages/Sitzplätze/jsobjects/Bff/metadata.json b/pages/Sitzplätze/jsobjects/Bff/metadata.json
new file mode 100644
index 0000000..0bb9b56
--- /dev/null
+++ b/pages/Sitzplätze/jsobjects/Bff/metadata.json
@@ -0,0 +1,14 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_0058b22e-bf9c-4341-b180-8a805ee21d6f",
+ "id": "Sitzplätze__$Bff$_Bff",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff",
+ "rootModuleInstanceId": "Sitzplätze_Bff",
+ "unpublishedCollection": {
+ "contextType": "PAGE",
+ "name": "_$Bff$_Bff",
+ "pageId": "Sitzplätze",
+ "pluginId": "js-plugin",
+ "pluginType": "JS"
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/jsobjects/BookingSelect/BookingSelect.js b/pages/Sitzplätze/jsobjects/BookingSelect/BookingSelect.js
index cdb2cb0..4554430 100644
--- a/pages/Sitzplätze/jsobjects/BookingSelect/BookingSelect.js
+++ b/pages/Sitzplätze/jsobjects/BookingSelect/BookingSelect.js
@@ -4,84 +4,77 @@ export default {
const selection = calendarModel.calendarSelection || null;
if (!selection) {
- showAlert(
- "Bitte zuerst einen Tag oder Zeitraum auswählen.",
- "warning"
- );
+ showAlert("Bitte zuerst einen Tag oder Zeitraum auswählen.", "warning");
return;
}
- function pad(value) {
- return String(value).padStart(2, "0");
- }
+ const pad = (value) => String(value).padStart(2, "0");
- function parseLocalDateTime(value) {
+ const parseLocalDateTime = (value) => {
if (!value) return null;
const text = String(value).trim();
- const parts = text.split(/[-T:]/g);
+ const match = text.match(
+ /^(\d{4})-(\d{2})-(\d{2})(?:T|\s)?(\d{2})?:?(\d{2})?:?(\d{2})?$/
+ );
- if (parts.length < 3) return null;
+ if (!match) return null;
const date = new Date(
- Number(parts[0]),
- Number(parts[1]) - 1,
- Number(parts[2]),
- Number(parts[3] || 0),
- Number(parts[4] || 0),
- Number(parts[5] || 0)
+ Number(match[1]),
+ Number(match[2]) - 1,
+ Number(match[3]),
+ Number(match[4] || 0),
+ Number(match[5] || 0),
+ Number(match[6] || 0)
);
return Number.isNaN(date.getTime()) ? null : date;
- }
+ };
- function formatDate(date) {
- return [
- date.getFullYear(),
- pad(date.getMonth() + 1),
- pad(date.getDate())
- ].join("-");
- }
+ const formatDate = (date) => [
+ date.getFullYear(),
+ pad(date.getMonth() + 1),
+ pad(date.getDate())
+ ].join("-");
- function formatTime(date) {
- return [
- pad(date.getHours()),
- pad(date.getMinutes())
- ].join(":");
- }
+ const formatTime = (date) => [
+ pad(date.getHours()),
+ pad(date.getMinutes())
+ ].join(":");
- function formatDateTimeForSql(date) {
- return `${formatDate(date)} ${formatTime(date)}:00`;
- }
+ const formatDateTimeForSql = (date) =>
+ `${formatDate(date)} ${formatTime(date)}:00`;
const start = parseLocalDateTime(selection.start);
const end = parseLocalDateTime(selection.end);
- if (!start || !end) {
+ if (!start || !end || end <= start) {
showAlert(
- "Die Kalenderauswahl enthält kein gültiges Datum.",
+ "Die Kalenderauswahl enthält kein gültiges Datum oder Zeitintervall.",
"error"
);
return;
}
+ const seatModel = Custom4.model || {};
+
const seatId = String(
- Custom4.model?.seatId ||
+ seatModel.seatId ||
+ seatModel.selectedSeat?.id ||
appsmith.store.selectedSeatId ||
""
).trim();
const seatName = String(
- Custom4.model?.seatName ||
+ seatModel.seatName ||
+ seatModel.selectedSeat?.name ||
appsmith.store.selectedSeatName ||
""
).trim();
if (!seatId) {
- showAlert(
- "Es wurde kein Sitzplatz ausgewählt.",
- "error"
- );
+ showAlert("Es wurde kein Sitzplatz ausgewählt.", "error");
return;
}
@@ -98,58 +91,86 @@ export default {
return;
}
- const isAllDay =
- selection.allDay === true ||
- selection.type === "day" ||
- selection.type === "days";
+ const isTimeSelection =
+ selection.type === "time" &&
+ selection.allDay !== true;
+ const isAllDay = !isTimeSelection;
+
+ let startDate;
let endDate;
let startTime = null;
let endTime = null;
- let endDateTime = end;
+ let startDateTime;
+ let endDateTime;
if (isAllDay) {
+ const startOfDay = new Date(start);
+ startOfDay.setHours(0, 0, 0, 0);
+
const lastSelectedDay = new Date(end);
- lastSelectedDay.setDate(lastSelectedDay.getDate() - 1);
+
+ // FullCalendar liefert bei Ganztagsauswahl ein exklusives Ende.
+ // Nur wenn end tatsächlich 00:00 Uhr ist, wird ein Tag abgezogen.
+ const endIsMidnight =
+ lastSelectedDay.getHours() === 0 &&
+ lastSelectedDay.getMinutes() === 0 &&
+ lastSelectedDay.getSeconds() === 0;
+
+ if (endIsMidnight) {
+ lastSelectedDay.setDate(
+ lastSelectedDay.getDate() - 1
+ );
+ }
+
+ lastSelectedDay.setHours(0, 0, 0, 0);
+
+ startDate = formatDate(startOfDay);
endDate = formatDate(lastSelectedDay);
- endDateTime = new Date(lastSelectedDay);
- endDateTime.setHours(23, 59, 59, 0);
+ startDateTime = `${startDate} 00:00:00`;
+ endDateTime = `${endDate} 23:59:59`;
} else {
- endDate = formatDate(end);
+ // Bei einem Timeslot bleiben Start- und Enddatum gleich.
+ startDate = formatDate(start);
+ endDate = formatDate(start);
startTime = formatTime(start);
endTime = formatTime(end);
+ startDateTime = formatDateTimeForSql(start);
+ endDateTime = formatDateTimeForSql(end);
}
- const recurringCheckbox =
- typeof RecurringCheckbox !== "undefined"
- ? RecurringCheckbox
- : null;
+ const recurring =
+ typeof RecurringCheckbox !== "undefined" &&
+ RecurringCheckbox.isChecked === true;
+
+ const recurringWeeks =
+ typeof RecurringWeeksSelect !== "undefined"
+ ? Number(RecurringWeeksSelect.selectedOptionValue || 1)
+ : 1;
+
+ const recurringDays = appsmith.store.recurringDays || {};
const bookingFormData = {
+ ...(appsmith.store.bookingFormData || {}),
seatId,
seatName,
bookedBy,
title: "Platzbuchung",
type: selection.type || null,
- startDate: formatDate(start),
+ startDate,
endDate,
startTime,
endTime,
- startDateTime: formatDateTimeForSql(start),
- endDateTime: formatDateTimeForSql(endDateTime),
- allDay: Boolean(isAllDay),
- recurring: Boolean(
- recurringCheckbox?.isChecked || false
- )
+ startDateTime,
+ endDateTime,
+ allDay: isAllDay,
+ recurring,
+ recurringWeeks,
+ recurringDays
};
try {
- await storeValue(
- "bookingFormData",
- bookingFormData,
- false
- );
-
+ await storeValue("bookingFormData", bookingFormData, false);
await showModal("Platzbuchung");
} catch (error) {
console.error(
diff --git a/pages/Sitzplätze/module instances/Bff.json b/pages/Sitzplätze/module instances/Bff.json
new file mode 100644
index 0000000..7b0db61
--- /dev/null
+++ b/pages/Sitzplätze/module instances/Bff.json
@@ -0,0 +1,64 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_59995a0e-1676-4d89-a2aa-cdb55c2ba464",
+ "id": "Sitzplätze_Bff",
+ "moduleUUID": "b0d6dbe1-9ee5-486f-9f93-4baa17986ccf",
+ "packageUUID": "35174735-e12c-426f-9f2f-fe12a068c3ba",
+ "type": "JS_MODULE",
+ "unpublishedModuleInstance": {
+ "contextType": "PAGE",
+ "inputs": {
+ "checkReturningFromLogin": "[\"\"]",
+ "clearSession": "[]",
+ "guardPage": "[\"Login\"]",
+ "isLoggedIn": "[]",
+ "login": "[\"\"]",
+ "logout": "[]"
+ },
+ "moduleInputs": {
+ "fkpwwxqbyo": {
+ "controlType": "INPUT_TEXT",
+ "defaultValue": "[]",
+ "id": "fkpwwxqbyo",
+ "label": "clearSession",
+ "propertyName": "inputs.clearSession"
+ },
+ "igfxnkxgzx": {
+ "controlType": "INPUT_TEXT",
+ "defaultValue": "[]",
+ "id": "igfxnkxgzx",
+ "label": "isLoggedIn",
+ "propertyName": "inputs.isLoggedIn"
+ },
+ "narkupnajw": {
+ "controlType": "INPUT_TEXT",
+ "defaultValue": "[]",
+ "id": "narkupnajw",
+ "label": "guardPage",
+ "propertyName": "inputs.guardPage"
+ },
+ "qqzhfbnjmf": {
+ "controlType": "INPUT_TEXT",
+ "defaultValue": "[]",
+ "id": "qqzhfbnjmf",
+ "label": "checkReturningFromLogin",
+ "propertyName": "inputs.checkReturningFromLogin"
+ },
+ "tlfzzixvrw": {
+ "controlType": "INPUT_TEXT",
+ "defaultValue": "[]",
+ "id": "tlfzzixvrw",
+ "label": "login",
+ "propertyName": "inputs.login"
+ },
+ "yjtnrwqqzm": {
+ "controlType": "INPUT_TEXT",
+ "defaultValue": "[]",
+ "id": "yjtnrwqqzm",
+ "label": "logout",
+ "propertyName": "inputs.logout"
+ }
+ },
+ "name": "Bff",
+ "pageId": "Sitzplätze"
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/module instances/Bff_Logout.json b/pages/Sitzplätze/module instances/Bff_Logout.json
new file mode 100644
index 0000000..9e880aa
--- /dev/null
+++ b/pages/Sitzplätze/module instances/Bff_Logout.json
@@ -0,0 +1,14 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_304a61c8-9fde-47e4-85a7-2056cd88ae28",
+ "id": "Sitzplätze_Bff_Logout",
+ "moduleUUID": "68741899-715d-4e34-a614-51281446403a",
+ "packageUUID": "35174735-e12c-426f-9f2f-fe12a068c3ba",
+ "type": "QUERY_MODULE",
+ "unpublishedModuleInstance": {
+ "contextType": "PAGE",
+ "inputs": {},
+ "moduleInputs": {},
+ "name": "Bff_Logout",
+ "pageId": "Sitzplätze"
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/module instances/Bff_Me.json b/pages/Sitzplätze/module instances/Bff_Me.json
new file mode 100644
index 0000000..008beaf
--- /dev/null
+++ b/pages/Sitzplätze/module instances/Bff_Me.json
@@ -0,0 +1,14 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_9e11ced6-24d2-4acb-9bfd-1969c6062191",
+ "id": "Sitzplätze_Bff_Me",
+ "moduleUUID": "809cd1d6-7b41-4561-bea0-2ac0794e1ac4",
+ "packageUUID": "35174735-e12c-426f-9f2f-fe12a068c3ba",
+ "type": "QUERY_MODULE",
+ "unpublishedModuleInstance": {
+ "contextType": "PAGE",
+ "inputs": {},
+ "moduleInputs": {},
+ "name": "Bff_Me",
+ "pageId": "Sitzplätze"
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/module instances/Bff_Verify.json b/pages/Sitzplätze/module instances/Bff_Verify.json
new file mode 100644
index 0000000..0100360
--- /dev/null
+++ b/pages/Sitzplätze/module instances/Bff_Verify.json
@@ -0,0 +1,14 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_060325a4-606f-44b7-817f-5d7e47cc8528",
+ "id": "Sitzplätze_Bff_Verify",
+ "moduleUUID": "83e73170-c955-4fa2-9414-3dec1f07d47c",
+ "packageUUID": "35174735-e12c-426f-9f2f-fe12a068c3ba",
+ "type": "QUERY_MODULE",
+ "unpublishedModuleInstance": {
+ "contextType": "PAGE",
+ "inputs": {},
+ "moduleInputs": {},
+ "name": "Bff_Verify",
+ "pageId": "Sitzplätze"
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff-checkReturningFromLogin/metadata.json b/pages/Sitzplätze/queries/Bff-checkReturningFromLogin/metadata.json
new file mode 100644
index 0000000..2475566
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff-checkReturningFromLogin/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_74de03ae-1d33-4e5c-8a1f-e27cbfc9b2a7",
+ "id": "Sitzplätze__$Bff$_Bff.checkReturningFromLogin",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff",
+ "pluginId": "js-plugin",
+ "pluginType": "JS",
+ "rootModuleInstanceId": "Sitzplätze_Bff",
+ "unpublishedAction": {
+ "collectionId": "Sitzplätze__$Bff$_Bff",
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "isAutoGenerated": false,
+ "name": "UNUSED_DATASOURCE",
+ "pluginId": "js-plugin"
+ },
+ "fullyQualifiedName": "_$Bff$_Bff.checkReturningFromLogin",
+ "name": "checkReturningFromLogin",
+ "pageId": "Sitzplätze",
+ "runBehaviour": "MANUAL",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff-clearSession/metadata.json b/pages/Sitzplätze/queries/Bff-clearSession/metadata.json
new file mode 100644
index 0000000..29584d4
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff-clearSession/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_0062be59-1821-4fd7-8cfe-c862f5883fa6",
+ "id": "Sitzplätze__$Bff$_Bff.clearSession",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff",
+ "pluginId": "js-plugin",
+ "pluginType": "JS",
+ "rootModuleInstanceId": "Sitzplätze_Bff",
+ "unpublishedAction": {
+ "collectionId": "Sitzplätze__$Bff$_Bff",
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "isAutoGenerated": false,
+ "name": "UNUSED_DATASOURCE",
+ "pluginId": "js-plugin"
+ },
+ "fullyQualifiedName": "_$Bff$_Bff.clearSession",
+ "name": "clearSession",
+ "pageId": "Sitzplätze",
+ "runBehaviour": "MANUAL",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff-guardPage/metadata.json b/pages/Sitzplätze/queries/Bff-guardPage/metadata.json
new file mode 100644
index 0000000..8e99ab3
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff-guardPage/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_bd00bbf2-3f8b-4cf2-9e75-b279493bc9ee",
+ "id": "Sitzplätze__$Bff$_Bff.guardPage",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff",
+ "pluginId": "js-plugin",
+ "pluginType": "JS",
+ "rootModuleInstanceId": "Sitzplätze_Bff",
+ "unpublishedAction": {
+ "collectionId": "Sitzplätze__$Bff$_Bff",
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "isAutoGenerated": false,
+ "name": "UNUSED_DATASOURCE",
+ "pluginId": "js-plugin"
+ },
+ "fullyQualifiedName": "_$Bff$_Bff.guardPage",
+ "name": "guardPage",
+ "pageId": "Sitzplätze",
+ "runBehaviour": "MANUAL",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff-isLoggedIn/metadata.json b/pages/Sitzplätze/queries/Bff-isLoggedIn/metadata.json
new file mode 100644
index 0000000..655e2c6
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff-isLoggedIn/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_01d53c11-6aca-429b-92b2-8e32e561f69a",
+ "id": "Sitzplätze__$Bff$_Bff.isLoggedIn",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff",
+ "pluginId": "js-plugin",
+ "pluginType": "JS",
+ "rootModuleInstanceId": "Sitzplätze_Bff",
+ "unpublishedAction": {
+ "collectionId": "Sitzplätze__$Bff$_Bff",
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "isAutoGenerated": false,
+ "name": "UNUSED_DATASOURCE",
+ "pluginId": "js-plugin"
+ },
+ "fullyQualifiedName": "_$Bff$_Bff.isLoggedIn",
+ "name": "isLoggedIn",
+ "pageId": "Sitzplätze",
+ "runBehaviour": "MANUAL",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff-login/metadata.json b/pages/Sitzplätze/queries/Bff-login/metadata.json
new file mode 100644
index 0000000..25d1b00
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff-login/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_cdad9604-64d3-4e80-9421-bab54d7453e5",
+ "id": "Sitzplätze__$Bff$_Bff.login",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff",
+ "pluginId": "js-plugin",
+ "pluginType": "JS",
+ "rootModuleInstanceId": "Sitzplätze_Bff",
+ "unpublishedAction": {
+ "collectionId": "Sitzplätze__$Bff$_Bff",
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "isAutoGenerated": false,
+ "name": "UNUSED_DATASOURCE",
+ "pluginId": "js-plugin"
+ },
+ "fullyQualifiedName": "_$Bff$_Bff.login",
+ "name": "login",
+ "pageId": "Sitzplätze",
+ "runBehaviour": "MANUAL",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff-logout/metadata.json b/pages/Sitzplätze/queries/Bff-logout/metadata.json
new file mode 100644
index 0000000..22ff14b
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff-logout/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_6c21e962-214e-40cc-8f51-c5921ccbad10",
+ "id": "Sitzplätze__$Bff$_Bff.logout",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff",
+ "pluginId": "js-plugin",
+ "pluginType": "JS",
+ "rootModuleInstanceId": "Sitzplätze_Bff",
+ "unpublishedAction": {
+ "collectionId": "Sitzplätze__$Bff$_Bff",
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "isAutoGenerated": false,
+ "name": "UNUSED_DATASOURCE",
+ "pluginId": "js-plugin"
+ },
+ "fullyQualifiedName": "_$Bff$_Bff.logout",
+ "name": "logout",
+ "pageId": "Sitzplätze",
+ "runBehaviour": "MANUAL",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff_Logout/metadata.json b/pages/Sitzplätze/queries/Bff_Logout/metadata.json
new file mode 100644
index 0000000..2c5651b
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff_Logout/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_df303cec-969d-4d02-bd28-9dd94884c95d",
+ "id": "Sitzplätze__$Bff_Logout$_Bff_Logout",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff_Logout",
+ "pluginId": "restapi-plugin",
+ "pluginType": "API",
+ "rootModuleInstanceId": "Sitzplätze_Bff_Logout",
+ "unpublishedAction": {
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "datasourceConfiguration": {
+ "url": "https://bff.ep-reisen.net"
+ },
+ "isAutoGenerated": false,
+ "name": "DEFAULT_REST_DATASOURCE",
+ "pluginId": "restapi-plugin"
+ },
+ "name": "_$Bff_Logout$_Bff_Logout",
+ "pageId": "Sitzplätze",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff_Me/metadata.json b/pages/Sitzplätze/queries/Bff_Me/metadata.json
new file mode 100644
index 0000000..3483ef2
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff_Me/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_bf1c83a9-9049-4779-abc2-393f2db249ca",
+ "id": "Sitzplätze__$Bff_Me$_Bff_Me",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff_Me",
+ "pluginId": "restapi-plugin",
+ "pluginType": "API",
+ "rootModuleInstanceId": "Sitzplätze_Bff_Me",
+ "unpublishedAction": {
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "datasourceConfiguration": {
+ "url": "https://bff.ep-reisen.net"
+ },
+ "isAutoGenerated": false,
+ "name": "DEFAULT_REST_DATASOURCE",
+ "pluginId": "restapi-plugin"
+ },
+ "name": "_$Bff_Me$_Bff_Me",
+ "pageId": "Sitzplätze",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/Bff_Verify/metadata.json b/pages/Sitzplätze/queries/Bff_Verify/metadata.json
new file mode 100644
index 0000000..bf6ce47
--- /dev/null
+++ b/pages/Sitzplätze/queries/Bff_Verify/metadata.json
@@ -0,0 +1,24 @@
+{
+ "gitSyncId": "6a6b5b477ebc9edeca96e2f8_0eaeb4b0-2fed-4629-8648-b369b6d5f223",
+ "id": "Sitzplätze__$Bff_Verify$_Bff_Verify",
+ "isPublic": true,
+ "moduleInstanceId": "Sitzplätze_Bff_Verify",
+ "pluginId": "restapi-plugin",
+ "pluginType": "API",
+ "rootModuleInstanceId": "Sitzplätze_Bff_Verify",
+ "unpublishedAction": {
+ "confirmBeforeExecute": false,
+ "contextType": "PAGE",
+ "datasource": {
+ "datasourceConfiguration": {
+ "url": "https://bff.ep-reisen.net"
+ },
+ "isAutoGenerated": false,
+ "name": "DEFAULT_REST_DATASOURCE",
+ "pluginId": "restapi-plugin"
+ },
+ "name": "_$Bff_Verify$_Bff_Verify",
+ "pageId": "Sitzplätze",
+ "userSetOnLoad": false
+ }
+}
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/CreateBooking/CreateBooking.txt b/pages/Sitzplätze/queries/CreateBooking/CreateBooking.txt
index 8c20bbf..73cfe03 100644
--- a/pages/Sitzplätze/queries/CreateBooking/CreateBooking.txt
+++ b/pages/Sitzplätze/queries/CreateBooking/CreateBooking.txt
@@ -8,9 +8,29 @@ INSERT INTO `buchungen`
)
VALUES
(
- {{ appsmith.store.bookingFormData.seatId }},
- {{ appsmith.store.bookingFormData.title }},
- {{ appsmith.store.bookingFormData.startDateTime }},
- {{ appsmith.store.bookingFormData.endDateTime }},
- {{ appsmith.store.bookingFormData.bookedBy }}
+ {{
+ appsmith.store.bookingFormData?.seatId ||
+ appsmith.store.selectedSeatId
+ }},
+
+ {{
+ appsmith.store.bookingFormData?.title ||
+ "Platzbuchung"
+ }},
+
+ {{
+ appsmith.store.bookingFormData?.allDay === true
+ ? appsmith.store.bookingFormData.startDate + " 00:00:00"
+ : appsmith.store.bookingFormData.startDateTime
+ }},
+
+ {{
+ appsmith.store.bookingFormData?.allDay === true
+ ? appsmith.store.bookingFormData.endDate + " 23:59:59"
+ : appsmith.store.bookingFormData.endDateTime
+ }},
+
+ {{
+ appsmith.store.bookingFormData?.bookedBy
+ }}
);
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/CreateBooking/metadata.json b/pages/Sitzplätze/queries/CreateBooking/metadata.json
index e3c1cad..2223b5a 100644
--- a/pages/Sitzplätze/queries/CreateBooking/metadata.json
+++ b/pages/Sitzplätze/queries/CreateBooking/metadata.json
@@ -5,7 +5,7 @@
"pluginType": "DB",
"unpublishedAction": {
"actionConfiguration": {
- "body": "INSERT INTO `buchungen`\n(\n `platz_id`,\n `titel`,\n `startzeit`,\n `endzeit`,\n `gebucht_von`\n)\nVALUES\n(\n {{ appsmith.store.bookingFormData.seatId }},\n {{ appsmith.store.bookingFormData.title }},\n {{ appsmith.store.bookingFormData.startDateTime }},\n {{ appsmith.store.bookingFormData.endDateTime }},\n {{ appsmith.store.bookingFormData.bookedBy }}\n);",
+ "body": "INSERT INTO `buchungen`\n(\n `platz_id`,\n `titel`,\n `startzeit`,\n `endzeit`,\n `gebucht_von`\n)\nVALUES\n(\n {{\n appsmith.store.bookingFormData?.seatId ||\n appsmith.store.selectedSeatId\n }},\n\n {{\n appsmith.store.bookingFormData?.title ||\n \"Platzbuchung\"\n }},\n\n {{\n appsmith.store.bookingFormData?.allDay === true\n ? appsmith.store.bookingFormData.startDate + \" 00:00:00\"\n : appsmith.store.bookingFormData.startDateTime\n }},\n\n {{\n appsmith.store.bookingFormData?.allDay === true\n ? appsmith.store.bookingFormData.endDate + \" 23:59:59\"\n : appsmith.store.bookingFormData.endDateTime\n }},\n\n {{\n appsmith.store.bookingFormData?.bookedBy\n }}\n);",
"encodeParamsToggle": true,
"paginationType": "NONE",
"pluginSpecifiedTemplates": [
diff --git a/pages/Sitzplätze/queries/DeleteBooking/DeleteBooking.txt b/pages/Sitzplätze/queries/DeleteBooking/DeleteBooking.txt
index f2411f9..b5e208c 100644
--- a/pages/Sitzplätze/queries/DeleteBooking/DeleteBooking.txt
+++ b/pages/Sitzplätze/queries/DeleteBooking/DeleteBooking.txt
@@ -1,5 +1,5 @@
DELETE FROM `buchungen`
-WHERE id =
- {{ this.params.bookingId }}
- AND gebucht_von =
- {{ this.params.bookedBy }};
\ No newline at end of file
+WHERE `id` =
+ {{ appsmith.store.selectedBooking.id }}
+ AND `gebucht_von` =
+ {{ appsmith.user.email }};
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/DeleteBooking/metadata.json b/pages/Sitzplätze/queries/DeleteBooking/metadata.json
index f6cd67e..8c6d050 100644
--- a/pages/Sitzplätze/queries/DeleteBooking/metadata.json
+++ b/pages/Sitzplätze/queries/DeleteBooking/metadata.json
@@ -5,7 +5,7 @@
"pluginType": "DB",
"unpublishedAction": {
"actionConfiguration": {
- "body": "DELETE FROM `buchungen`\nWHERE id =\n {{ this.params.bookingId }}\n AND gebucht_von =\n {{ this.params.bookedBy }};",
+ "body": "DELETE FROM `buchungen`\nWHERE `id` =\n {{ appsmith.store.selectedBooking.id }}\n AND `gebucht_von` =\n {{ appsmith.user.email }};",
"encodeParamsToggle": true,
"paginationType": "NONE",
"pluginSpecifiedTemplates": [
diff --git a/pages/Sitzplätze/queries/GetSeatBookings/GetSeatBookings.txt b/pages/Sitzplätze/queries/GetSeatBookings/GetSeatBookings.txt
index 018c107..1bedc0e 100644
--- a/pages/Sitzplätze/queries/GetSeatBookings/GetSeatBookings.txt
+++ b/pages/Sitzplätze/queries/GetSeatBookings/GetSeatBookings.txt
@@ -2,11 +2,17 @@ SELECT
id,
platz_id,
titel AS title,
- startzeit AS start,
- endzeit AS end,
+ DATE_FORMAT(
+ startzeit,
+ '%Y-%m-%d %H:%i:%s'
+ ) AS start,
+ DATE_FORMAT(
+ endzeit,
+ '%Y-%m-%d %H:%i:%s'
+ ) AS end,
gebucht_von,
erstellt_am
FROM `buchungen`
-WHERE platz_id =
- {{ appsmith.store.selectedSeatId }}
+WHERE `platz_id` =
+ '{{ appsmith.store.selectedSeatId }}'
ORDER BY startzeit;
\ No newline at end of file
diff --git a/pages/Sitzplätze/queries/GetSeatBookings/metadata.json b/pages/Sitzplätze/queries/GetSeatBookings/metadata.json
index 277f5dd..fc0d6d5 100644
--- a/pages/Sitzplätze/queries/GetSeatBookings/metadata.json
+++ b/pages/Sitzplätze/queries/GetSeatBookings/metadata.json
@@ -5,7 +5,7 @@
"pluginType": "DB",
"unpublishedAction": {
"actionConfiguration": {
- "body": "SELECT\n id,\n platz_id,\n titel AS title,\n startzeit AS start,\n endzeit AS end,\n gebucht_von,\n erstellt_am\nFROM `buchungen`\nWHERE platz_id =\n {{ appsmith.store.selectedSeatId }}\nORDER BY startzeit;",
+ "body": "SELECT\n id,\n platz_id,\n titel AS title,\n DATE_FORMAT(\n startzeit,\n '%Y-%m-%d %H:%i:%s'\n ) AS start,\n DATE_FORMAT(\n endzeit,\n '%Y-%m-%d %H:%i:%s'\n ) AS end,\n gebucht_von,\n erstellt_am\nFROM `buchungen`\nWHERE `platz_id` =\n '{{ appsmith.store.selectedSeatId }}'\nORDER BY startzeit;",
"encodeParamsToggle": true,
"paginationType": "NONE",
"pluginSpecifiedTemplates": [
diff --git a/pages/Sitzplätze/widgets/Custom4.json b/pages/Sitzplätze/widgets/Custom4.json
index 06699a1..7d9da39 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 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}}",
+ "seatSelected": "{{\n storeValue(\n \"selectedSeatId\",\n Custom4.model?.selectedSeat?.id ||\n Custom4.model?.seatId ||\n null\n )\n .then(() =>\n storeValue(\n \"selectedSeatName\",\n Custom4.model?.selectedSeat?.name ||\n Custom4.model?.seatName ||\n null\n )\n )\n .then(() =>\n showModal(\n SeatCalendar.name\n )\n )\n}}",
"srcDoc": {
"css": "@charset \"UTF-8\";\nhtml,\nbody {\n margin: 0;\n 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/MyBookingDetailsModal.json b/pages/Sitzplätze/widgets/MyBookingDetailsModal/MyBookingDetailsModal.json
index e575a20..451038b 100644
--- a/pages/Sitzplätze/widgets/MyBookingDetailsModal/MyBookingDetailsModal.json
+++ b/pages/Sitzplätze/widgets/MyBookingDetailsModal/MyBookingDetailsModal.json
@@ -1,14 +1,14 @@
{
"animateLoading": true,
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
- "bottomRow": 289,
+ "bottomRow": 309,
"boxShadow": "none",
"canEscapeKeyClose": true,
"canOutsideClickClose": true,
"children": [
{
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
- "bottomRow": 260,
+ "bottomRow": 280,
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"canExtend": true,
"detachFromLayout": true,
@@ -57,14 +57,14 @@
}
],
"dynamicHeight": "AUTO_HEIGHT",
- "height": 260,
+ "height": 280,
"isCanvas": true,
"isLoading": false,
"key": "jc8yhgzuwd",
"leftColumn": 20,
"maxDynamicHeight": 9000,
"minDynamicHeight": 24,
- "minHeight": 260,
+ "minHeight": 280,
"mobileBottomRow": 53,
"mobileLeftColumn": 20,
"mobileRightColumn": 44,
diff --git a/pages/Sitzplätze/widgets/MyBookingDetailsModal/Text6.json b/pages/Sitzplätze/widgets/MyBookingDetailsModal/Text6.json
index df19a20..07a10fe 100644
--- a/pages/Sitzplätze/widgets/MyBookingDetailsModal/Text6.json
+++ b/pages/Sitzplätze/widgets/MyBookingDetailsModal/Text6.json
@@ -1,7 +1,7 @@
{
"animateLoading": true,
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
- "bottomRow": 24,
+ "bottomRow": 26,
"dynamicBindingPathList": [
{
"key": "truncateButtonColor"
diff --git a/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json b/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json
index 8fb56ce..c8c4786 100644
--- a/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json
+++ b/pages/Sitzplätze/widgets/SeatCalendar/Custom5.json
@@ -32,6 +32,9 @@
},
{
"key": "bookingClicked"
+ },
+ {
+ "key": "timeSelected"
}
],
"dynamicTriggerPathList": [
@@ -72,20 +75,20 @@
"parentRowSpace": 10,
"renderMode": "CANVAS",
"rightColumn": 64,
- "seatSelected": "{{\n storeValue(\n \"selectedSeatId\",\n Custom4.model.selectedSeat.id\n )\n .then(() =>\n storeValue(\n \"selectedSeatName\",\n Custom4.model.selectedSeat.name\n )\n )\n .then(() => CreateBooking.run())\n .then(() => showModal(SeatCalendar.name))\n}}",
+ "seatSelected": "{{\n storeValue(\n \"selectedSeat\",\n Custom4.model?.selectedSeat || null\n )\n .then(() =>\n storeValue(\n \"selectedSeatId\",\n Custom4.model?.selectedSeat?.id ||\n Custom4.model?.seatId ||\n null\n )\n )\n .then(() =>\n storeValue(\n \"selectedSeatName\",\n Custom4.model?.selectedSeat?.name ||\n Custom4.model?.seatName ||\n null\n )\n )\n .then(() =>\n showModal(\n SeatCalendar.name\n )\n )\n}}",
"srcDoc": {
"css": "@charset \"UTF-8\";\nhtml,\nbody {\n margin: 0;\n padding: 0;\n width: 100%;\n min-height: 100%;\n font-family: Arial, sans-serif;\n overflow: hidden;\n}\n\n/* Äußerer Custom-Widget-Bereich */\n#calendarWrapper {\n width: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: 10px;\n background: #ffffff;\n overflow: visible;\n}\n\n/* Kopfbereich */\n#calendarHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 10px;\n padding: 8px 10px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n background: #f9fafb;\n}\n\n#calendarSeatTitle {\n font-size: 15px;\n font-weight: 600;\n color: #374151;\n}\n\n/* Kalender */\n#calendar {\n width: 100%;\n min-height: 0;\n box-sizing: border-box;\n --calendar-selection-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n --fc-highlight-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n --fc-today-bg-color: transparent;\n}\n\n/* Auswahltext */\n#calendarSelection {\n margin-top: 10px;\n padding: 8px 10px;\n border: 1px solid #d1d5db;\n border-radius: 5px;\n background: #f9fafb;\n color: #374151;\n font-size: 13px;\n}\n\n/* FullCalendar-Grundformatierung */\n#calendar .fc {\n width: 100%;\n font-size: 13px;\n --fc-highlight-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n --fc-today-bg-color: transparent;\n}\n\n#calendar .fc .fc-button {\n background: #374151;\n border-color: #374151;\n}\n\n#calendar .fc .fc-button:hover,\n#calendar .fc .fc-button-active {\n background: #111827;\n border-color: #111827;\n}\n\n#calendar .fc .fc-event {\n cursor: pointer;\n}\n\n/*\n * EINHEITLICHE AUSWAHLFARBE\n *\n * Gilt für:\n * - einen Tag\n * - mehrere Tage\n * - einen Timeslot\n *\n * Wichtig: FullCalendar verwendet background.\n */\n#calendar .fc .fc-highlight,\n#calendar .fc .fc-highlight-skeleton,\n#calendar .fc .fc-highlight-skeleton td,\n#calendar .fc .fc-highlight-container,\n#calendar .fc .fc-highlight-container td {\n background: var(--calendar-selection-color) !important;\n background-color: var(--calendar-selection-color) !important;\n opacity: 1 !important;\n border: 0 !important;\n box-shadow: none !important;\n}\n\n/*\n * Tagesansicht:\n * mögliche Auswahlcontainer ebenfalls\n * auf dieselbe Farbe setzen.\n */\n#calendar .fc-timegrid .fc-timegrid-col-bg,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container table,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container td,\n#calendar .fc-timegrid .fc-highlight-skeleton,\n#calendar .fc-timegrid .fc-highlight-container {\n background: transparent !important;\n background-color: transparent !important;\n}\n\n/*\n * Die tatsächliche Auswahlfläche\n * in der Tagesansicht.\n */\n#calendar .fc-timegrid .fc-highlight,\n#calendar .fc-timegrid .fc-highlight-skeleton td,\n#calendar .fc-timegrid .fc-highlight-container td {\n background: var(--calendar-selection-color) !important;\n background-color: var(--calendar-selection-color) !important;\n opacity: 1 !important;\n border: 0 !important;\n box-shadow: none !important;\n}\n\n/*\n * Aktueller Tag in der Monatsansicht:\n * transparenter Hintergrund,\n * nur schmaler roter Rahmen.\n */\n#calendar .fc-daygrid-day.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: inset 0 0 0 1px #dc2626 !important;\n}\n\n#calendar .fc-daygrid-day.fc-day-today .fc-daygrid-day-frame {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n}\n\n/*\n * Kopfzelle des aktuellen Tages\n * ohne Hintergrund und ohne Rahmen.\n */\n#calendar .fc-col-header-cell.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n}\n\n/*\n * Tagesansicht:\n * aktueller Tag ohne Hintergrund\n * und ohne roten Rahmen.\n */\n#calendar .fc-timegrid-col.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n\n#calendar .fc-timegrid-col.fc-day-today .fc-timegrid-col-frame {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n\n/* Wochenenden grau */\n#calendar td.fc-day-sat,\n#calendar td.fc-day-sun,\n#calendar .fc-timegrid-col.fc-day-sat,\n#calendar .fc-timegrid-col.fc-day-sun {\n background-color: #eeeeee !important;\n}\n\n/* Wochenend-Kopfzellen */\n#calendar .fc-col-header-cell.fc-day-sat,\n#calendar .fc-col-header-cell.fc-day-sun {\n background-color: #d6d6d6 !important;\n}\n\n/*\n * Monatsansicht:\n * kein Scrollen innerhalb des Monats.\n */\n#calendar .fc-daygrid .fc-scroller {\n height: auto !important;\n max-height: none !important;\n overflow-y: visible !important;\n overflow-x: visible !important;\n}\n\n/*\n * Tagesansicht:\n * nur das Zeitraster scrollt.\n */\n#calendar .fc-timegrid .fc-scroller {\n overflow-y: auto !important;\n overflow-x: hidden !important;\n overscroll-behavior: contain;\n}\n\n/* TimeGrid-Grundstruktur */\n#calendar .fc-timegrid {\n min-height: 0 !important;\n}\n\n#calendar .fc-timegrid .fc-view-harness {\n min-height: 0 !important;\n}\n\n/* Tabellenbreite */\n#calendar .fc-scrollgrid {\n width: 100%;\n max-width: 100%;\n}\n\n/* Größere 30-Minuten-Zeilen */\n#calendar .fc-timegrid-slot {\n height: 30px !important;\n min-height: 30px !important;\n}\n\n#calendar .fc-timegrid-slot-label {\n height: 30px !important;\n min-height: 30px !important;\n}\n\n/*\n * Falls ein dunkelblauer Mirror-Event\n * weiterhin sichtbar ist.\n *\n * Diese Regeln wirken nur auf temporäre\n * Auswahl-Events, nicht auf echte Buchungen.\n */\n#calendar .fc-event-mirror,\n#calendar .fc-timegrid-event-mirror,\n#calendar .fc-timegrid-event-harness-mirror {\n background: var(--calendar-selection-color) !important;\n background-color: var(--calendar-selection-color) !important;\n border-color: transparent !important;\n color: transparent !important;\n opacity: 1 !important;\n box-shadow: none !important;\n}",
"html": "\n \n\n
\n\n
\n Keine Auswahl\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 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});"
+ "js": "let calendar = null;\nlet resizeTimer = null;\nlet modelChangeRegistered = false;\nlet resizeRegistered = false;\nconst MONTH_HEIGHT = 620;\nconst MONTH_CONTENT_HEIGHT = 550;\nconst DAY_HEIGHT = 600;\nconst DAY_CONTENT_HEIGHT = 480;\nconst BUSINESS_START_MINUTES = 7 * 60;\nconst BUSINESS_END_MINUTES = 18 * 60;\nconst SLOT_MINUTES = 30;\nfunction getModel() {\n const model = appsmith.model || {};\n if (typeof model === \"string\") {\n try {\n return JSON.parse(model);\n } catch (error) {\n return {};\n }\n }\n return model;\n}\nfunction pad(value) {\n return String(value).padStart(2, \"0\");\n}\nfunction formatDate(date) {\n return [date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join(\"-\");\n}\nfunction formatLocalDateTime(date) {\n return `${formatDate(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;\n}\nfunction getDateOnly(value) {\n return String(value || \"\").replace(\"T\", \" \").slice(0, 10);\n}\nfunction dateFromDateOnly(value) {\n const parts = getDateOnly(value).split(\"-\").map(Number);\n return new Date(parts[0], parts[1] - 1, parts[2]);\n}\nfunction isWeekendDate(date) {\n return date.getDay() === 0 || date.getDay() === 6;\n}\nfunction isWeekend(date) {\n return isWeekendDate(date);\n}\nfunction isBusinessDay(date) {\n return date.getDay() >= 1 && date.getDay() <= 5;\n}\nfunction getMinutesOfDay(date) {\n return date.getHours() * 60 + date.getMinutes();\n}\nfunction getWeekdaysBetween(startDate, exclusiveEndDate) {\n const result = [];\n const current = new Date(startDate);\n while (current < exclusiveEndDate) {\n if (!isWeekendDate(current)) result.push(formatDate(current));\n current.setDate(current.getDate() + 1);\n }\n return result;\n}\nfunction makeAllDaySegment(booking, start, end) {\n return {\n id: String(booking.id),\n title: booking.title || booking.titel || \"Platzbuchung\",\n start: `${formatDate(start)}T00:00:00`,\n end: `${formatDate(end)}T00:00:00`,\n allDay: true,\n extendedProps: {\n id: String(booking.id),\n originalBookingId: String(booking.id),\n platz_id: booking.platz_id,\n gebucht_von: booking.gebucht_von,\n allDay: true\n }\n };\n}\nfunction createAllDayEvent(booking) {\n const startDate = getDateOnly(booking.start);\n const endDate = getDateOnly(booking.end);\n const start = dateFromDateOnly(startDate);\n const exclusiveEnd = dateFromDateOnly(endDate);\n const rawEnd = String(booking.end || \"\");\n if (rawEnd.indexOf(\"23:59:59\") >= 0 || exclusiveEnd.getTime() === start.getTime()) {\n exclusiveEnd.setDate(exclusiveEnd.getDate() + 1);\n }\n const weekdays = getWeekdaysBetween(start, exclusiveEnd);\n if (!weekdays.length) return [];\n const events = [];\n let segmentStart = dateFromDateOnly(weekdays[0]);\n let previous = new Date(segmentStart);\n for (let index = 1; index < weekdays.length; index += 1) {\n const current = dateFromDateOnly(weekdays[index]);\n const expected = new Date(previous);\n expected.setDate(expected.getDate() + 1);\n if (current.getTime() !== expected.getTime()) {\n const segmentEnd = new Date(previous);\n segmentEnd.setDate(segmentEnd.getDate() + 1);\n events.push(makeAllDaySegment(booking, segmentStart, segmentEnd));\n segmentStart = new Date(current);\n }\n previous = new Date(current);\n }\n const finalEnd = new Date(previous);\n finalEnd.setDate(finalEnd.getDate() + 1);\n events.push(makeAllDaySegment(booking, segmentStart, finalEnd));\n return events;\n}\nfunction createCalendarEvents(bookings, selectedSeatId) {\n return bookings.filter(booking => {\n const bookingSeatId = String(booking.platz_id || booking.extendedProps && booking.extendedProps.platz_id || \"\").trim();\n return bookingSeatId === String(selectedSeatId).trim();\n }).reduce((result, booking) => {\n const allDay = booking.allDay === true || booking.allDay === \"true\" || booking.startTime == null && booking.endTime == null;\n if (allDay) return result.concat(createAllDayEvent(booking));\n result.push({\n id: String(booking.id),\n title: booking.title || booking.titel || \"Platzbuchung\",\n start: String(booking.start || \"\").replace(\" \", \"T\").replace(/Z$/, \"\"),\n end: String(booking.end || \"\").replace(\" \", \"T\").replace(/Z$/, \"\"),\n allDay: false,\n extendedProps: {\n id: String(booking.id),\n originalBookingId: String(booking.id),\n platz_id: booking.platz_id,\n gebucht_von: booking.gebucht_von,\n allDay: false\n }\n });\n return result;\n }, []);\n}\nfunction getEvents() {\n const model = getModel();\n const selectedSeatId = String(model.seatId || model.selectedSeatId || \"\").trim();\n const bookings = Array.isArray(model.events) ? model.events : [];\n return createCalendarEvents(bookings, selectedSeatId);\n}\nfunction getSeatName() {\n const model = getModel();\n return model.seatName || model.seatId || \"Kein Platz ausgewählt\";\n}\nfunction getCurrentUser() {\n const model = getModel();\n return String(model.currentUser || \"\").trim().toLowerCase();\n}\nfunction updateSeatTitle() {\n const element = document.getElementById(\"calendarSeatTitle\");\n if (element) element.textContent = `Kalender für ${getSeatName()}`;\n}\nfunction isDayView() {\n return Boolean(calendar && calendar.view && calendar.view.type === \"timeGridDay\");\n}\nfunction updateSelectionText(text) {\n const element = document.getElementById(\"calendarSelection\");\n if (!element) return;\n if (isDayView()) {\n element.textContent = \"\";\n element.style.display = \"none\";\n return;\n }\n element.style.display = \"block\";\n element.textContent = text;\n}\nfunction updateCalendarViewSettings(viewType) {\n if (!calendar) return;\n const dayView = viewType === \"timeGridDay\";\n calendar.setOption(\"height\", dayView ? DAY_HEIGHT : MONTH_HEIGHT);\n calendar.setOption(\"contentHeight\", dayView ? DAY_CONTENT_HEIGHT : MONTH_CONTENT_HEIGHT);\n calendar.setOption(\"expandRows\", !dayView);\n if (typeof calendar.updateSize === \"function\") calendar.updateSize();\n}\nfunction scheduleResize() {\n window.clearTimeout(resizeTimer);\n resizeTimer = window.setTimeout(() => {\n if (calendar) calendar.updateSize();\n }, 250);\n}\nfunction isBusinessTimeRange(start, end) {\n if (!start || !end || !isBusinessDay(start)) return false;\n if (start.toDateString() !== end.toDateString()) return false;\n return getMinutesOfDay(start) >= BUSINESS_START_MINUTES && getMinutesOfDay(end) <= BUSINESS_END_MINUTES && getMinutesOfDay(end) > getMinutesOfDay(start);\n}\nfunction isValidAllDaySelection(start, end) {\n return Boolean(start && end && end > start);\n}\nfunction showWeekendMessage() {\n updateSelectionText(\"Samstag und Sonntag sind nicht buchbar.\");\n}\nfunction showBusinessHoursMessage() {\n updateSelectionText(\"Reservierungen sind Montag bis Freitag von 07:00 bis 18:00 Uhr möglich.\");\n}\nfunction openDayView(date) {\n if (!calendar) return;\n if (isWeekend(date)) {\n showWeekendMessage();\n return;\n }\n calendar.changeView(\"timeGridDay\", date);\n}\nfunction isDayNumberClick(info) {\n const target = info.jsEvent && info.jsEvent.target;\n return Boolean(target && typeof target.closest === \"function\" && target.closest(\".fc-daygrid-day-number\"));\n}\nfunction roundToHalfHour(date) {\n const result = new Date(date);\n result.setSeconds(0, 0);\n result.setMinutes(result.getMinutes() < 30 ? 0 : 30);\n return result;\n}\nfunction formatDateTime(date) {\n if (!date) return \"\";\n return new Intl.DateTimeFormat(\"de-DE\", {\n dateStyle: \"short\",\n timeStyle: \"short\"\n }).format(date);\n}\nfunction triggerAppsmithEvent(eventName) {\n try {\n appsmith.triggerEvent(eventName);\n } catch (error) {\n console.warn(`Event ${eventName} ist nicht konfiguriert.`, error);\n }\n}\nfunction normalizeBookingEvent(event) {\n const extended = event.extendedProps || {};\n return {\n id: extended.originalBookingId || extended.id || event.id || null,\n platz_id: extended.platz_id || event.platz_id || null,\n title: event.title || extended.title || \"Platzbuchung\",\n start: event.start ? formatLocalDateTime(event.start) : null,\n end: event.end ? formatLocalDateTime(event.end) : null,\n gebucht_von: extended.gebucht_von || event.gebucht_von || null,\n allDay: event.allDay === true || extended.allDay === true\n };\n}\nfunction isOwnBooking(booking) {\n const currentUser = getCurrentUser();\n const bookingUser = String(booking.gebucht_von || \"\").trim().toLowerCase();\n return Boolean(currentUser && bookingUser && currentUser === bookingUser);\n}\nfunction handleBookingClick(info) {\n const booking = normalizeBookingEvent(info.event);\n if (!booking.id) {\n updateSelectionText(\"Diese Buchung besitzt keine ID und kann nicht gelöscht werden.\");\n return;\n }\n if (!isOwnBooking(booking)) {\n triggerAppsmithEvent(\"foreignBookingClicked\");\n updateSelectionText(\"Diese Buchung gehört einem anderen Benutzer.\");\n return;\n }\n appsmith.updateModel({\n selectedBooking: booking\n });\n updateSelectionText(`${booking.title} – ${info.event.allDay ? \"Ganztägig\" : formatDateTime(info.event.start)}`);\n triggerAppsmithEvent(\"bookingClicked\");\n}\nfunction buildCalendarSelection(type, start, end) {\n const allDay = type === \"day\" || type === \"days\";\n return {\n type: String(type),\n start: formatLocalDateTime(start),\n end: formatLocalDateTime(end),\n allDay,\n selectedDays: allDay ? Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)) : null\n };\n}\nfunction publishCalendarSelection(type, start, end) {\n if (!start || !end) return;\n appsmith.updateModel({\n calendarSelection: buildCalendarSelection(type, start, end)\n });\n if (type === \"day\") updateSelectionText(`Ausgewählter Tag: ${formatDateTime(start)}`);else if (type === \"days\") updateSelectionText(`Ausgewählte Tage: ${formatDateTime(start)} bis ${formatDateTime(new Date(end.getTime() - 1))}`);else updateSelectionText(`Ausgewählt: ${formatDateTime(start)} bis ${formatDateTime(end)}`);\n triggerAppsmithEvent(type === \"time\" ? \"timeSelected\" : \"bookingSelected\");\n}\nfunction selectSingleMonthDay(date) {\n if (!calendar || !date) return;\n if (isWeekend(date)) {\n calendar.unselect();\n showWeekendMessage();\n return;\n }\n const start = new Date(date);\n start.setHours(0, 0, 0, 0);\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n calendar.unselect();\n window.setTimeout(() => {\n if (calendar) calendar.select({\n start,\n end,\n allDay: true\n });\n }, 0);\n}\nfunction createCalendar() {\n const calendarElement = document.getElementById(\"calendar\");\n if (!calendarElement) return;\n if (typeof FullCalendar === \"undefined\") {\n calendarElement.textContent = \"FullCalendar konnte nicht geladen werden.\";\n return;\n }\n if (calendar) {\n calendar.destroy();\n calendar = null;\n }\n calendar = new FullCalendar.Calendar(calendarElement, {\n initialView: \"dayGridMonth\",\n timeZone: \"local\",\n locale: \"de\",\n firstDay: 1,\n displayEventTime: false,\n allDaySlot: true,\n businessHours: {\n daysOfWeek: [1, 2, 3, 4, 5],\n startTime: \"07:00\",\n endTime: \"18:00\"\n },\n navLinks: true,\n navLinkDayClick(date) {\n openDayView(date);\n },\n dateClick(info) {\n const clickedDate = info.date;\n if (info.view.type === \"dayGridMonth\") {\n if (isDayNumberClick(info)) {\n openDayView(clickedDate);\n return;\n }\n selectSingleMonthDay(clickedDate);\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n if (isWeekend(clickedDate)) {\n showWeekendMessage();\n return;\n }\n const startSlot = roundToHalfHour(clickedDate);\n const endSlot = new Date(startSlot.getTime() + SLOT_MINUTES * 60 * 1000);\n if (!isBusinessTimeRange(startSlot, endSlot)) {\n showBusinessHoursMessage();\n return;\n }\n calendar.unselect();\n calendar.select({\n start: startSlot,\n end: endSlot,\n allDay: false\n });\n }\n },\n selectable: true,\n selectMirror: false,\n unselectAuto: false,\n selectMinDistance: 5,\n selectAllow(info) {\n if (info.allDay) return isValidAllDaySelection(info.start, info.end);\n return isBusinessTimeRange(info.start, info.end);\n },\n editable: false,\n eventStartEditable: false,\n eventDurationEditable: false,\n select(info) {\n if (info.view.type === \"dayGridMonth\") {\n const days = Math.round((info.end.getTime() - info.start.getTime()) / (24 * 60 * 60 * 1000));\n publishCalendarSelection(days === 1 ? \"day\" : \"days\", info.start, info.end);\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n if (!isBusinessTimeRange(info.start, info.end)) {\n calendar.unselect();\n showBusinessHoursMessage();\n return;\n }\n publishCalendarSelection(\"time\", info.start, info.end);\n }\n },\n unselect() {\n const element = document.getElementById(\"calendarSelection\");\n if (isDayView()) {\n if (element) {\n element.textContent = \"\";\n element.style.display = \"none\";\n }\n return;\n }\n if (element) {\n element.style.display = \"block\";\n element.textContent = \"Keine Auswahl\";\n }\n appsmith.updateModel({\n calendarSelection: null\n });\n },\n height: MONTH_HEIGHT,\n contentHeight: MONTH_CONTENT_HEIGHT,\n expandRows: true,\n headerToolbar: {\n left: \"prev,next today\",\n center: \"title\",\n right: \"dayGridMonth,timeGridDay\"\n },\n buttonText: {\n today: \"Heute\",\n month: \"Monat\",\n day: \"Tag\"\n },\n slotDuration: \"00:30:00\",\n snapDuration: \"00:30:00\",\n slotMinTime: \"07:00:00\",\n slotMaxTime: \"22:00:00\",\n scrollTime: \"08:00:00\",\n scrollTimeReset: false,\n nowIndicator: true,\n events: getEvents(),\n eventClick(info) {\n handleBookingClick(info);\n },\n eventDidMount(info) {\n const booking = normalizeBookingEvent(info.event);\n info.el.style.cursor = booking.id && isOwnBooking(booking) ? \"pointer\" : \"default\";\n if (booking.id && isOwnBooking(booking)) info.el.title = \"Eigene Buchung anklicken, um Details zu öffnen\";\n },\n datesSet(info) {\n updateCalendarViewSettings(info.view.type);\n }\n });\n calendar.render();\n updateCalendarViewSettings(calendar.view.type);\n}\nfunction refreshCalendarEvents() {\n if (!calendar) {\n createCalendar();\n return;\n }\n calendar.removeAllEvents();\n calendar.addEventSource(getEvents());\n calendar.updateSize();\n}\nfunction registerResizeHandling() {\n if (resizeRegistered) return;\n resizeRegistered = true;\n window.addEventListener(\"resize\", scheduleResize);\n}\nappsmith.onReady(() => {\n updateSeatTitle();\n createCalendar();\n registerResizeHandling();\n if (!modelChangeRegistered) {\n modelChangeRegistered = true;\n appsmith.onModelChange(() => {\n updateSeatTitle();\n refreshCalendarEvents();\n });\n }\n});"
},
"theme": "{{appsmith.theme}}",
- "timeSelected": "{{showModal(Platzbuchung.name);}}",
+ "timeSelected": "{{\n BookingSelect.openBookingWidget()\n}}",
"topRow": 0,
"type": "CUSTOM_WIDGET",
"uncompiledSrcDoc": {
"css": "html,\nbody {\n margin: 0;\n padding: 0;\n width: 100%;\n min-height: 100%;\n font-family: Arial, sans-serif;\n overflow: hidden;\n}\n\n/* Äußerer Custom-Widget-Bereich */\n\n#calendarWrapper {\n width: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: 10px;\n background: #ffffff;\n overflow: visible;\n}\n\n/* Kopfbereich */\n\n#calendarHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 10px;\n padding: 8px 10px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n background: #f9fafb;\n}\n\n#calendarSeatTitle {\n font-size: 15px;\n font-weight: 600;\n color: #374151;\n}\n\n/* Kalender */\n\n#calendar {\n width: 100%;\n min-height: 0;\n box-sizing: border-box;\n\n --calendar-selection-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n\n --fc-highlight-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n\n --fc-today-bg-color: transparent;\n}\n\n/* Auswahltext */\n\n#calendarSelection {\n margin-top: 10px;\n padding: 8px 10px;\n border: 1px solid #d1d5db;\n border-radius: 5px;\n background: #f9fafb;\n color: #374151;\n font-size: 13px;\n}\n\n/* FullCalendar-Grundformatierung */\n\n#calendar .fc {\n width: 100%;\n font-size: 13px;\n\n --fc-highlight-color: rgba(\n 11,\n 116,\n 222,\n 0.12\n );\n\n --fc-today-bg-color: transparent;\n}\n\n#calendar .fc .fc-button {\n background: #374151;\n border-color: #374151;\n}\n\n#calendar .fc .fc-button:hover,\n#calendar .fc .fc-button-active {\n background: #111827;\n border-color: #111827;\n}\n\n#calendar .fc .fc-event {\n cursor: pointer;\n}\n\n/*\n * EINHEITLICHE AUSWAHLFARBE\n *\n * Gilt für:\n * - einen Tag\n * - mehrere Tage\n * - einen Timeslot\n *\n * Wichtig: FullCalendar verwendet background.\n */\n#calendar .fc .fc-highlight,\n#calendar .fc .fc-highlight-skeleton,\n#calendar .fc .fc-highlight-skeleton td,\n#calendar .fc .fc-highlight-container,\n#calendar .fc .fc-highlight-container td {\n background: var(\n --calendar-selection-color\n ) !important;\n\n background-color: var(\n --calendar-selection-color\n ) !important;\n\n opacity: 1 !important;\n border: 0 !important;\n box-shadow: none !important;\n}\n\n/*\n * Tagesansicht:\n * mögliche Auswahlcontainer ebenfalls\n * auf dieselbe Farbe setzen.\n */\n#calendar .fc-timegrid .fc-timegrid-col-bg,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container table,\n#calendar .fc-timegrid .fc-timegrid-col-bg-container td,\n#calendar .fc-timegrid .fc-highlight-skeleton,\n#calendar .fc-timegrid .fc-highlight-container {\n background: transparent !important;\n background-color: transparent !important;\n}\n\n/*\n * Die tatsächliche Auswahlfläche\n * in der Tagesansicht.\n */\n#calendar .fc-timegrid .fc-highlight,\n#calendar .fc-timegrid .fc-highlight-skeleton td,\n#calendar .fc-timegrid .fc-highlight-container td {\n background: var(\n --calendar-selection-color\n ) !important;\n\n background-color: var(\n --calendar-selection-color\n ) !important;\n\n opacity: 1 !important;\n border: 0 !important;\n box-shadow: none !important;\n}\n\n/*\n * Aktueller Tag in der Monatsansicht:\n * transparenter Hintergrund,\n * nur schmaler roter Rahmen.\n */\n#calendar .fc-daygrid-day.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: inset 0 0 0 1px #dc2626 !important;\n}\n\n#calendar\n .fc-daygrid-day.fc-day-today\n .fc-daygrid-day-frame {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n}\n\n/*\n * Kopfzelle des aktuellen Tages\n * ohne Hintergrund und ohne Rahmen.\n */\n#calendar .fc-col-header-cell.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n}\n\n/*\n * Tagesansicht:\n * aktueller Tag ohne Hintergrund\n * und ohne roten Rahmen.\n */\n#calendar .fc-timegrid-col.fc-day-today {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n\n#calendar\n .fc-timegrid-col.fc-day-today\n .fc-timegrid-col-frame {\n background: transparent !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n\n/* Wochenenden grau */\n\n#calendar td.fc-day-sat,\n#calendar td.fc-day-sun,\n#calendar .fc-timegrid-col.fc-day-sat,\n#calendar .fc-timegrid-col.fc-day-sun {\n background-color: #eeeeee !important;\n}\n\n/* Wochenend-Kopfzellen */\n\n#calendar .fc-col-header-cell.fc-day-sat,\n#calendar .fc-col-header-cell.fc-day-sun {\n background-color: #d6d6d6 !important;\n}\n\n/*\n * Monatsansicht:\n * kein Scrollen innerhalb des Monats.\n */\n#calendar .fc-daygrid .fc-scroller {\n height: auto !important;\n max-height: none !important;\n overflow-y: visible !important;\n overflow-x: visible !important;\n}\n\n/*\n * Tagesansicht:\n * nur das Zeitraster scrollt.\n */\n#calendar .fc-timegrid .fc-scroller {\n overflow-y: auto !important;\n overflow-x: hidden !important;\n overscroll-behavior: contain;\n}\n\n/* TimeGrid-Grundstruktur */\n\n#calendar .fc-timegrid {\n min-height: 0 !important;\n}\n\n#calendar .fc-timegrid .fc-view-harness {\n min-height: 0 !important;\n}\n\n/* Tabellenbreite */\n\n#calendar .fc-scrollgrid {\n width: 100%;\n max-width: 100%;\n}\n\n/* Größere 30-Minuten-Zeilen */\n\n#calendar .fc-timegrid-slot {\n height: 30px !important;\n min-height: 30px !important;\n}\n\n#calendar .fc-timegrid-slot-label {\n height: 30px !important;\n min-height: 30px !important;\n}\n\n/*\n * Falls ein dunkelblauer Mirror-Event\n * weiterhin sichtbar ist.\n *\n * Diese Regeln wirken nur auf temporäre\n * Auswahl-Events, nicht auf echte Buchungen.\n */\n#calendar .fc-event-mirror,\n#calendar .fc-timegrid-event-mirror,\n#calendar .fc-timegrid-event-harness-mirror {\n background: var(\n --calendar-selection-color\n ) !important;\n\n background-color: var(\n --calendar-selection-color\n ) !important;\n\n border-color: transparent !important;\n color: transparent !important;\n opacity: 1 !important;\n box-shadow: none !important;\n}",
"html": "\n \n\n
\n\n
\n Keine Auswahl\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;\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});"
+ "js": "let calendar = null;\nlet resizeTimer = null;\nlet modelChangeRegistered = false;\nlet resizeRegistered = false;\n\nconst MONTH_HEIGHT = 620;\nconst MONTH_CONTENT_HEIGHT = 550;\nconst DAY_HEIGHT = 600;\nconst DAY_CONTENT_HEIGHT = 480;\nconst BUSINESS_START_MINUTES = 7 * 60;\nconst BUSINESS_END_MINUTES = 18 * 60;\nconst SLOT_MINUTES = 30;\n\nfunction getModel() {\n const model = appsmith.model || {};\n if (typeof model === \"string\") {\n try { return JSON.parse(model); } catch (error) { return {}; }\n }\n return model;\n}\n\nfunction pad(value) { return String(value).padStart(2, \"0\"); }\nfunction formatDate(date) { return [date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join(\"-\"); }\nfunction formatLocalDateTime(date) { return `${formatDate(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }\nfunction getDateOnly(value) { return String(value || \"\").replace(\"T\", \" \").slice(0, 10); }\nfunction dateFromDateOnly(value) { const parts = getDateOnly(value).split(\"-\").map(Number); return new Date(parts[0], parts[1] - 1, parts[2]); }\nfunction isWeekendDate(date) { return date.getDay() === 0 || date.getDay() === 6; }\nfunction isWeekend(date) { return isWeekendDate(date); }\nfunction isBusinessDay(date) { return date.getDay() >= 1 && date.getDay() <= 5; }\nfunction getMinutesOfDay(date) { return date.getHours() * 60 + date.getMinutes(); }\n\nfunction getWeekdaysBetween(startDate, exclusiveEndDate) {\n const result = [];\n const current = new Date(startDate);\n while (current < exclusiveEndDate) {\n if (!isWeekendDate(current)) result.push(formatDate(current));\n current.setDate(current.getDate() + 1);\n }\n return result;\n}\n\nfunction makeAllDaySegment(booking, start, end) {\n return {\n id: String(booking.id),\n title: booking.title || booking.titel || \"Platzbuchung\",\n start: `${formatDate(start)}T00:00:00`,\n end: `${formatDate(end)}T00:00:00`,\n allDay: true,\n extendedProps: {\n id: String(booking.id),\n originalBookingId: String(booking.id),\n platz_id: booking.platz_id,\n gebucht_von: booking.gebucht_von,\n allDay: true\n }\n };\n}\n\nfunction createAllDayEvent(booking) {\n const startDate = getDateOnly(booking.start);\n const endDate = getDateOnly(booking.end);\n const start = dateFromDateOnly(startDate);\n const exclusiveEnd = dateFromDateOnly(endDate);\n const rawEnd = String(booking.end || \"\");\n\n if (rawEnd.indexOf(\"23:59:59\") >= 0 || exclusiveEnd.getTime() === start.getTime()) {\n exclusiveEnd.setDate(exclusiveEnd.getDate() + 1);\n }\n\n const weekdays = getWeekdaysBetween(start, exclusiveEnd);\n if (!weekdays.length) return [];\n\n const events = [];\n let segmentStart = dateFromDateOnly(weekdays[0]);\n let previous = new Date(segmentStart);\n\n for (let index = 1; index < weekdays.length; index += 1) {\n const current = dateFromDateOnly(weekdays[index]);\n const expected = new Date(previous);\n expected.setDate(expected.getDate() + 1);\n\n if (current.getTime() !== expected.getTime()) {\n const segmentEnd = new Date(previous);\n segmentEnd.setDate(segmentEnd.getDate() + 1);\n events.push(makeAllDaySegment(booking, segmentStart, segmentEnd));\n segmentStart = new Date(current);\n }\n\n previous = new Date(current);\n }\n\n const finalEnd = new Date(previous);\n finalEnd.setDate(finalEnd.getDate() + 1);\n events.push(makeAllDaySegment(booking, segmentStart, finalEnd));\n return events;\n}\n\nfunction createCalendarEvents(bookings, selectedSeatId) {\n return bookings\n .filter((booking) => {\n const bookingSeatId = String(\n booking.platz_id ||\n (booking.extendedProps && booking.extendedProps.platz_id) ||\n \"\"\n ).trim();\n return bookingSeatId === String(selectedSeatId).trim();\n })\n .reduce((result, booking) => {\n const allDay = booking.allDay === true || booking.allDay === \"true\" || (booking.startTime == null && booking.endTime == null);\n if (allDay) return result.concat(createAllDayEvent(booking));\n\n result.push({\n id: String(booking.id),\n title: booking.title || booking.titel || \"Platzbuchung\",\n start: String(booking.start || \"\").replace(\" \", \"T\").replace(/Z$/, \"\"),\n end: String(booking.end || \"\").replace(\" \", \"T\").replace(/Z$/, \"\"),\n allDay: false,\n extendedProps: {\n id: String(booking.id),\n originalBookingId: String(booking.id),\n platz_id: booking.platz_id,\n gebucht_von: booking.gebucht_von,\n allDay: false\n }\n });\n return result;\n }, []);\n}\n\nfunction getEvents() {\n const model = getModel();\n const selectedSeatId = String(model.seatId || model.selectedSeatId || \"\").trim();\n const bookings = Array.isArray(model.events) ? model.events : [];\n return createCalendarEvents(bookings, selectedSeatId);\n}\n\nfunction getSeatName() { const model = getModel(); return model.seatName || model.seatId || \"Kein Platz ausgewählt\"; }\nfunction getCurrentUser() { const model = getModel(); return String(model.currentUser || \"\").trim().toLowerCase(); }\nfunction updateSeatTitle() { const element = document.getElementById(\"calendarSeatTitle\"); if (element) element.textContent = `Kalender für ${getSeatName()}`; }\nfunction isDayView() { return Boolean(calendar && calendar.view && calendar.view.type === \"timeGridDay\"); }\n\nfunction updateSelectionText(text) {\n const element = document.getElementById(\"calendarSelection\");\n if (!element) return;\n if (isDayView()) { element.textContent = \"\"; element.style.display = \"none\"; return; }\n element.style.display = \"block\";\n element.textContent = text;\n}\n\nfunction updateCalendarViewSettings(viewType) {\n if (!calendar) return;\n const dayView = viewType === \"timeGridDay\";\n calendar.setOption(\"height\", dayView ? DAY_HEIGHT : MONTH_HEIGHT);\n calendar.setOption(\"contentHeight\", dayView ? DAY_CONTENT_HEIGHT : MONTH_CONTENT_HEIGHT);\n calendar.setOption(\"expandRows\", !dayView);\n if (typeof calendar.updateSize === \"function\") calendar.updateSize();\n}\n\nfunction scheduleResize() {\n window.clearTimeout(resizeTimer);\n resizeTimer = window.setTimeout(() => { if (calendar) calendar.updateSize(); }, 250);\n}\n\nfunction isBusinessTimeRange(start, end) {\n if (!start || !end || !isBusinessDay(start)) return false;\n if (start.toDateString() !== end.toDateString()) return false;\n return getMinutesOfDay(start) >= BUSINESS_START_MINUTES && getMinutesOfDay(end) <= BUSINESS_END_MINUTES && getMinutesOfDay(end) > getMinutesOfDay(start);\n}\n\nfunction isValidAllDaySelection(start, end) {\n return Boolean(start && end && end > start);\n}\n\nfunction showWeekendMessage() { updateSelectionText(\"Samstag und Sonntag sind nicht buchbar.\"); }\nfunction showBusinessHoursMessage() { updateSelectionText(\"Reservierungen sind Montag bis Freitag von 07:00 bis 18:00 Uhr möglich.\"); }\nfunction openDayView(date) { if (!calendar) return; if (isWeekend(date)) { showWeekendMessage(); return; } calendar.changeView(\"timeGridDay\", date); }\n\nfunction isDayNumberClick(info) {\n const target = info.jsEvent && info.jsEvent.target;\n return Boolean(target && typeof target.closest === \"function\" && target.closest(\".fc-daygrid-day-number\"));\n}\n\nfunction roundToHalfHour(date) {\n const result = new Date(date);\n result.setSeconds(0, 0);\n result.setMinutes(result.getMinutes() < 30 ? 0 : 30);\n return result;\n}\n\nfunction formatDateTime(date) {\n if (!date) return \"\";\n return new Intl.DateTimeFormat(\"de-DE\", { dateStyle: \"short\", timeStyle: \"short\" }).format(date);\n}\n\nfunction triggerAppsmithEvent(eventName) {\n try { appsmith.triggerEvent(eventName); } catch (error) { console.warn(`Event ${eventName} ist nicht konfiguriert.`, error); }\n}\n\nfunction normalizeBookingEvent(event) {\n const extended = event.extendedProps || {};\n return {\n id: extended.originalBookingId || extended.id || event.id || null,\n platz_id: extended.platz_id || event.platz_id || null,\n title: event.title || extended.title || \"Platzbuchung\",\n start: event.start ? formatLocalDateTime(event.start) : null,\n end: event.end ? formatLocalDateTime(event.end) : null,\n gebucht_von: extended.gebucht_von || event.gebucht_von || null,\n allDay: event.allDay === true || extended.allDay === true\n };\n}\n\nfunction isOwnBooking(booking) {\n const currentUser = getCurrentUser();\n const bookingUser = String(booking.gebucht_von || \"\").trim().toLowerCase();\n return Boolean(currentUser && bookingUser && currentUser === bookingUser);\n}\n\nfunction handleBookingClick(info) {\n const booking = normalizeBookingEvent(info.event);\n if (!booking.id) { updateSelectionText(\"Diese Buchung besitzt keine ID und kann nicht gelöscht werden.\"); return; }\n if (!isOwnBooking(booking)) { triggerAppsmithEvent(\"foreignBookingClicked\"); updateSelectionText(\"Diese Buchung gehört einem anderen Benutzer.\"); return; }\n appsmith.updateModel({ selectedBooking: booking });\n updateSelectionText(`${booking.title} – ${info.event.allDay ? \"Ganztägig\" : formatDateTime(info.event.start)}`);\n triggerAppsmithEvent(\"bookingClicked\");\n}\n\nfunction buildCalendarSelection(type, start, end) {\n const allDay = type === \"day\" || type === \"days\";\n return { type: String(type), start: formatLocalDateTime(start), end: formatLocalDateTime(end), allDay, selectedDays: allDay ? Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)) : null };\n}\n\nfunction publishCalendarSelection(type, start, end) {\n if (!start || !end) return;\n appsmith.updateModel({ calendarSelection: buildCalendarSelection(type, start, end) });\n if (type === \"day\") updateSelectionText(`Ausgewählter Tag: ${formatDateTime(start)}`);\n else if (type === \"days\") updateSelectionText(`Ausgewählte Tage: ${formatDateTime(start)} bis ${formatDateTime(new Date(end.getTime() - 1))}`);\n else updateSelectionText(`Ausgewählt: ${formatDateTime(start)} bis ${formatDateTime(end)}`);\n triggerAppsmithEvent(type === \"time\" ? \"timeSelected\" : \"bookingSelected\");\n}\n\nfunction selectSingleMonthDay(date) {\n if (!calendar || !date) return;\n if (isWeekend(date)) { calendar.unselect(); showWeekendMessage(); return; }\n const start = new Date(date); start.setHours(0, 0, 0, 0);\n const end = new Date(start); end.setDate(end.getDate() + 1);\n calendar.unselect();\n window.setTimeout(() => { if (calendar) calendar.select({ start, end, allDay: true }); }, 0);\n}\n\nfunction createCalendar() {\n const calendarElement = document.getElementById(\"calendar\");\n if (!calendarElement) return;\n if (typeof FullCalendar === \"undefined\") { calendarElement.textContent = \"FullCalendar konnte nicht geladen werden.\"; return; }\n if (calendar) { calendar.destroy(); calendar = null; }\n\n calendar = new FullCalendar.Calendar(calendarElement, {\n initialView: \"dayGridMonth\",\n timeZone: \"local\",\n locale: \"de\",\n firstDay: 1,\n displayEventTime: false,\n allDaySlot: true,\n businessHours: { daysOfWeek: [1, 2, 3, 4, 5], startTime: \"07:00\", endTime: \"18:00\" },\n navLinks: true,\n navLinkDayClick(date) { openDayView(date); },\n\n dateClick(info) {\n const clickedDate = info.date;\n if (info.view.type === \"dayGridMonth\") {\n if (isDayNumberClick(info)) { openDayView(clickedDate); return; }\n selectSingleMonthDay(clickedDate);\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n if (isWeekend(clickedDate)) { showWeekendMessage(); return; }\n const startSlot = roundToHalfHour(clickedDate);\n const endSlot = new Date(startSlot.getTime() + SLOT_MINUTES * 60 * 1000);\n if (!isBusinessTimeRange(startSlot, endSlot)) { showBusinessHoursMessage(); return; }\n calendar.unselect();\n calendar.select({ start: startSlot, end: endSlot, allDay: false });\n }\n },\n\n selectable: true,\n selectMirror: false,\n unselectAuto: false,\n selectMinDistance: 5,\n selectAllow(info) {\n if (info.allDay) return isValidAllDaySelection(info.start, info.end);\n return isBusinessTimeRange(info.start, info.end);\n },\n editable: false,\n eventStartEditable: false,\n eventDurationEditable: false,\n\n select(info) {\n if (info.view.type === \"dayGridMonth\") {\n const days = Math.round((info.end.getTime() - info.start.getTime()) / (24 * 60 * 60 * 1000));\n publishCalendarSelection(days === 1 ? \"day\" : \"days\", info.start, info.end);\n return;\n }\n if (info.view.type === \"timeGridDay\") {\n if (!isBusinessTimeRange(info.start, info.end)) { calendar.unselect(); showBusinessHoursMessage(); return; }\n publishCalendarSelection(\"time\", info.start, info.end);\n }\n },\n\n unselect() {\n const element = document.getElementById(\"calendarSelection\");\n if (isDayView()) { if (element) { element.textContent = \"\"; element.style.display = \"none\"; } return; }\n if (element) { element.style.display = \"block\"; element.textContent = \"Keine Auswahl\"; }\n appsmith.updateModel({ calendarSelection: null });\n },\n\n height: MONTH_HEIGHT,\n contentHeight: MONTH_CONTENT_HEIGHT,\n expandRows: true,\n headerToolbar: { left: \"prev,next today\", center: \"title\", right: \"dayGridMonth,timeGridDay\" },\n buttonText: { today: \"Heute\", month: \"Monat\", day: \"Tag\" },\n slotDuration: \"00:30:00\",\n snapDuration: \"00:30:00\",\n slotMinTime: \"07:00:00\",\n slotMaxTime: \"22:00:00\",\n scrollTime: \"08:00:00\",\n scrollTimeReset: false,\n nowIndicator: true,\n events: getEvents(),\n eventClick(info) { handleBookingClick(info); },\n eventDidMount(info) {\n const booking = normalizeBookingEvent(info.event);\n info.el.style.cursor = booking.id && isOwnBooking(booking) ? \"pointer\" : \"default\";\n if (booking.id && isOwnBooking(booking)) info.el.title = \"Eigene Buchung anklicken, um Details zu öffnen\";\n },\n datesSet(info) { updateCalendarViewSettings(info.view.type); }\n });\n\n calendar.render();\n updateCalendarViewSettings(calendar.view.type);\n}\n\nfunction refreshCalendarEvents() {\n if (!calendar) { createCalendar(); return; }\n calendar.removeAllEvents();\n calendar.addEventSource(getEvents());\n calendar.updateSize();\n}\n\nfunction registerResizeHandling() {\n if (resizeRegistered) return;\n resizeRegistered = true;\n window.addEventListener(\"resize\", scheduleResize);\n}\n\nappsmith.onReady(() => {\n updateSeatTitle();\n createCalendar();\n registerResizeHandling();\n if (!modelChangeRegistered) {\n modelChangeRegistered = true;\n appsmith.onModelChange(() => { updateSeatTitle(); refreshCalendarEvents(); });\n }\n});"
},
"version": 1,
"widgetId": "ad7fob2vs9",
diff --git a/pages/Sitzplätze/widgets/Text7.json b/pages/Sitzplätze/widgets/Text7.json
new file mode 100644
index 0000000..74aed80
--- /dev/null
+++ b/pages/Sitzplätze/widgets/Text7.json
@@ -0,0 +1,55 @@
+{
+ "animateLoading": true,
+ "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
+ "bottomRow": 27,
+ "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": "u9yq2qfrlw",
+ "leftColumn": 0,
+ "maxDynamicHeight": 9000,
+ "minDynamicHeight": 4,
+ "minWidth": 450,
+ "mobileBottomRow": 27,
+ "mobileLeftColumn": 0,
+ "mobileRightColumn": 13,
+ "mobileTopRow": 23,
+ "needsErrorInfo": false,
+ "originalBottomRow": 27,
+ "originalTopRow": 23,
+ "overflow": "NONE",
+ "parentColumnSpace": 25.390625,
+ "parentId": "0",
+ "parentRowSpace": 10,
+ "renderMode": "CANVAS",
+ "responsiveBehavior": "fill",
+ "rightColumn": 13,
+ "shouldTruncate": false,
+ "text": "{{appsmith.store.sid, appsmith.store.permissions, appsmith.store.user}}\n",
+ "textAlign": "LEFT",
+ "textColor": "#231F20",
+ "topRow": 23,
+ "truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}",
+ "type": "TEXT_WIDGET",
+ "version": 1,
+ "widgetId": "m3zjevyiib",
+ "widgetName": "Text7"
+}
\ No newline at end of file
diff --git a/source modules/Bff/Bff.json b/source modules/Bff/Bff.json
new file mode 100644
index 0000000..fbb9c18
--- /dev/null
+++ b/source modules/Bff/Bff.json
@@ -0,0 +1,6 @@
+{
+ "moduleName": "Bff",
+ "moduleUUID": "b0d6dbe1-9ee5-486f-9f93-4baa17986ccf",
+ "packageName": "Bff",
+ "packageUUID": "35174735-e12c-426f-9f2f-fe12a068c3ba"
+}
\ No newline at end of file
diff --git a/source modules/Bff/Bff_Logout.json b/source modules/Bff/Bff_Logout.json
new file mode 100644
index 0000000..9133aec
--- /dev/null
+++ b/source modules/Bff/Bff_Logout.json
@@ -0,0 +1,6 @@
+{
+ "moduleName": "Bff_Logout",
+ "moduleUUID": "68741899-715d-4e34-a614-51281446403a",
+ "packageName": "Bff",
+ "packageUUID": "35174735-e12c-426f-9f2f-fe12a068c3ba"
+}
\ No newline at end of file
diff --git a/source modules/Bff/Bff_Me.json b/source modules/Bff/Bff_Me.json
new file mode 100644
index 0000000..5027d79
--- /dev/null
+++ b/source modules/Bff/Bff_Me.json
@@ -0,0 +1,6 @@
+{
+ "moduleName": "Bff_Me",
+ "moduleUUID": "809cd1d6-7b41-4561-bea0-2ac0794e1ac4",
+ "packageName": "Bff",
+ "packageUUID": "35174735-e12c-426f-9f2f-fe12a068c3ba"
+}
\ No newline at end of file
diff --git a/source modules/Bff/Bff_Verify.json b/source modules/Bff/Bff_Verify.json
new file mode 100644
index 0000000..ff71d58
--- /dev/null
+++ b/source modules/Bff/Bff_Verify.json
@@ -0,0 +1,6 @@
+{
+ "moduleName": "Bff_Verify",
+ "moduleUUID": "83e73170-c955-4fa2-9414-3dec1f07d47c",
+ "packageName": "Bff",
+ "packageUUID": "35174735-e12c-426f-9f2f-fe12a068c3ba"
+}
\ No newline at end of file