-
- {{ "Required inputs are marked with *." }}
-
-
-
-
-
-
-
-
+
+
+ {{ "Required inputs are marked with *." }}
+
`,
};
diff --git a/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyCalendar.js b/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyCalendar.js
index 5604e05d5..4caaca674 100644
--- a/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyCalendar.js
+++ b/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyCalendar.js
@@ -1,18 +1,324 @@
+import CoodleSurveyCalendarTimeslotCard from "./CoodleSurveyCalendar/CoodleSurveyCalendarTimeslotCard.js";
+import FhcCalendar from "../../../../Calendar/Base.js";
+import ModeWeek from "../../../../Calendar/Mode/Week.js";
+
+import { numberPadding } from "../../../../../helpers/DateHelpers.js";
+
export default {
name: "CoodleSurveyCalendar",
- components: {},
- props: {},
+ components: { CoodleSurveyCalendarTimeslotCard, FhcCalendar },
+ inject: ["renderers"],
+ props: {
+ surveyFormData: Object | null,
+ },
emits: [],
data() {
- return {}
+ return {
+ mode: "Week",
+ modes: {
+ week: Vue.markRaw(ModeWeek),
+ },
+ modeOptions: {
+ week: {
+ collapseEmptyDays: false,
+ },
+ },
+ timeslots: [
+ {
+ type: "coodle",
+ datum: "2026-06-24",
+ beginn: "10:45:00",
+ ende: "12:00:00",
+ isostart: "2026-06-24T08:45:00.000Z",
+ isoend: "2026-06-24T10:00:00.000Z",
+ farbe: "FFFFFF",
+ },
+ {
+ type: "coodle",
+ datum: "2026-06-25",
+ beginn: "14:55:00",
+ ende: "16:10:00",
+ isostart: "2026-06-25T12:55:00.000Z",
+ isoend: "2026-06-25T14:10:00.000Z",
+ farbe: "FFFFFF",
+ },
+ {
+ type: "coodle",
+ datum: "2026-06-25",
+ beginn: "12:05:00",
+ ende: "13:20:00",
+ isostart: "2026-06-25T10:05:00.000Z",
+ isoend: "2026-06-25T11:20:00.000Z",
+ farbe: "FFFFFF",
+ },
+ {
+ type: "coodle",
+ datum: "2026-06-26",
+ beginn: "14:20:00",
+ ende: "15:35:00",
+ isostart: "2026-06-26T12:20:00.000Z",
+ isoend: "2026-06-26T13:35:00.000Z",
+ farbe: "FFFFFF",
+ },
+ {
+ type: "coodle",
+ datum: "2026-06-24",
+ beginn: "14:35:00",
+ ende: "15:50:00",
+ isostart: "2026-06-24T12:35:00.000Z",
+ isoend: "2026-06-24T13:50:00.000Z",
+ farbe: "FFFFFF",
+ },
+ {
+ type: "coodle",
+ datum: "2026-06-24",
+ beginn: "17:20:00",
+ ende: "18:35:00",
+ isostart: "2026-06-24T15:20:00.000Z",
+ isoend: "2026-06-24T16:35:00.000Z",
+ farbe: "FFFFFF",
+ },
+ {
+ type: "coodle",
+ datum: "2026-06-25",
+ beginn: "17:30:00",
+ ende: "18:45:00",
+ isostart: "2026-06-25T15:30:00.000Z",
+ isoend: "2026-06-25T16:45:00.000Z",
+ farbe: "FFFFFF",
+ },
+ {
+ type: "coodle",
+ datum: "2026-06-26",
+ beginn: "18:40:00",
+ ende: "19:55:00",
+ isostart: "2026-06-26T16:40:00.000Z",
+ isoend: "2026-06-26T17:55:00.000Z",
+ farbe: "FFFFFF",
+ },
+ ],
+ isTimeslotCardEditInProgress: false,
+ };
+ },
+ computed: {
+ timezone() {
+ return FHC_JS_DATA_STORAGE_OBJECT.timezone;
+ },
+ backgrounds() {
+ let now = luxon.DateTime.now().setZone(this.timezone);
+
+ return [
+ {
+ class: "background-past",
+ end: now,
+ label: now.startOf("minute").toISOTime({
+ suppressSeconds: true,
+ includeOffset: false,
+ }),
+ },
+ ];
+ },
+ events() {
+ // todo
+ return this.timeslots;
+ },
+ roundedTimeslotDuration() {
+ return (
+ this.$props.surveyFormData.timeslotDuration -
+ (this.$props.surveyFormData.timeslotDuration % 5)
+ );
+ },
+ },
+ watch: {
+ "surveyFormData.timeslotDuration": {
+ handler() {
+ this.updateDurationOfExistingTimeslots();
+ },
+ },
+ timeslots: {
+ handler() {
+ // todo: update form data
+ },
+ deep: true,
+ },
+ },
+ methods: {
+ addTimeslotOnCalendarClick(clickEvent) {
+ if (this.formatTimeslotForCalendarBasedOnStart.length >= 50) {
+ return;
+ }
+
+ const elementRectangle = clickEvent.target.getBoundingClientRect();
+ const relativeClickLocation = clickEvent.y - elementRectangle.top;
+ let minutes =
+ Math.floor(
+ (relativeClickLocation / elementRectangle.height) * 12,
+ ) * 5;
+ if (minutes < 0 || minutes > 55) {
+ minutes = 0;
+ }
+
+ let hours = clickEvent.params?.part?.values?.hours ?? 12;
+ if (hours < 7 || hours > 22) {
+ hours = 12;
+ }
+
+ let start = new Date(clickEvent.params.date.ts);
+ start.setHours(hours);
+ start.setMinutes(minutes);
+
+ let newTimeslot = this.formatTimeslotForCalendarBasedOnStart(start);
+
+ if (
+ this.timeslots.some(
+ (timeslot) => timeslot.isostart === newTimeslot.isostart,
+ )
+ ) {
+ return;
+ }
+
+ this.timeslots.push(newTimeslot);
+ this.sortTimeslots();
+ },
+ formatTimeslotForCalendarBasedOnStart(start) {
+ let end = new Date(start.getTime());
+ end.setMinutes(end.getMinutes() + this.roundedTimeslotDuration);
+
+ return {
+ type: "coodle",
+ datum: start.toISOString().slice(0, 10),
+ beginn:
+ numberPadding(start.getHours()) +
+ ":" +
+ numberPadding(start.getMinutes()) +
+ ":00",
+ ende:
+ numberPadding(end.getHours()) +
+ ":" +
+ numberPadding(end.getMinutes()) +
+ ":00",
+ isostart: start.toISOString(),
+ isoend: end.toISOString(),
+ farbe: "FFFFFF",
+ };
+ },
+ updateDurationOfExistingTimeslots() {
+ this.timeslots = this.timeslots.map((timeslot) => {
+ let start = new Date(timeslot.isostart);
+ return this.formatTimeslotForCalendarBasedOnStart(start);
+ });
+ this.sortTimeslots();
+ },
+ createTimeslotFromCard(startDate) {
+ if (
+ this.timeslots.some(
+ (existingTimeslot) =>
+ existingTimeslot.isostart ===
+ startDate.toISOString(),
+ )
+ ) {
+ window.alert("You cannot create duplicates!");
+ return;
+ }
+
+ this.timeslots.push(this.formatTimeslotForCalendarBasedOnStart(startDate));
+ this.sortTimeslots();
+ this.isTimeslotCardEditInProgress = false;
+ },
+ updateTimeslotFromCard(timeslot, newStartDate) {
+ if (
+ this.timeslots.some(
+ (existingTimeslot) =>
+ existingTimeslot.isostart ===
+ newStartDate.toISOString(),
+ )
+ ) {
+ window.alert("You cannot create duplicates!");
+ return;
+ }
+
+ this.timeslots = this.timeslots.map((existingTimeslot) => {
+ if (existingTimeslot.isostart !== timeslot.isostart) {
+ return existingTimeslot;
+ }
+
+ return this.formatTimeslotForCalendarBasedOnStart(newStartDate);
+ });
+
+ this.sortTimeslots();
+ this.isTimeslotCardEditInProgress = false;
+ },
+ deleteTimeslot(timeslot) {
+ this.timeslots = this.timeslots.filter((existingTimeslot) => {
+ return existingTimeslot.isostart !== timeslot.isostart;
+ });
+ this.sortTimeslots();
+ this.isTimeslotCardEditInProgress = false;
+ },
+ sortTimeslots() {
+ this.timeslots = this.timeslots.sort((timeslotA, timeslotB) => {
+ let timeslotAStartDateTime = timeslotA.datum + timeslotA.beginn;
+ let timeslotBStartDateTime = timeslotB.datum + timeslotB.beginn;
+ if (timeslotAStartDateTime > timeslotBStartDateTime) {
+ return 1;
+ } else {
+ return -1;
+ }
+ });
+ },
+ },
+ created() {
+ // todo: set timeslots
+ this.sortTimeslots();
},
- computed: {},
- watch: {},
- methods: {},
- created() {},
template: /*html*/ `
-
- calendar placeholder
+
+
+ {{ "Timeslot selection" }}
+ {{ "You can add timeslots directly to the calendar or from the list down below." }}
+
+
+
`,
-}
\ No newline at end of file
+};
diff --git a/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyCalendar/CoodleSurveyCalendarTimeslotCard.js b/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyCalendar/CoodleSurveyCalendarTimeslotCard.js
new file mode 100644
index 000000000..b852cf7dc
--- /dev/null
+++ b/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyCalendar/CoodleSurveyCalendarTimeslotCard.js
@@ -0,0 +1,192 @@
+import { numberPadding } from "../../../../../../helpers/DateHelpers.js";
+
+export default {
+ name: "CoodleSurveyCalendarTimeslotCard",
+ props: {
+ timeslot: Object | null,
+ isTimeslotCardEditInProgress: Boolean,
+ },
+ emits: [
+ "deleteTimeslot",
+ "updateTimeslot",
+ "setIsTimeslotCardEditInProgress",
+ ],
+ data() {
+ return {
+ isEditInProgress: false,
+ dateInput: null,
+ startTimeInput: null,
+ minStartTimeInput: "07:00",
+ maxStartTimeInput: "22:55",
+ };
+ },
+ computed: {
+ formattedDate() {
+ let dateFragments = this.$props.timeslot.datum.split("-");
+ return dateFragments[2] + "." + dateFragments[1] + ".";
+ },
+ formattedStartEnd() {
+ return (
+ this.$props.timeslot?.beginn?.slice(0, 5) +
+ "-" +
+ this.$props.timeslot?.ende?.slice(0, 5)
+ );
+ },
+ },
+ watch: {
+ timeslot: {
+ handler() {
+ this.isEditInProgress = false;
+ this.setInputs();
+ },
+ deep: true,
+ },
+ isEditInProgress() {
+ this.setInputs();
+ },
+ isTimeslotCardEditInProgress() {
+ this.isEditInProgress = false;
+ },
+ },
+ methods: {
+ setInputs() {
+ this.dateInput = this.$props.timeslot?.datum;
+ this.startTimeInput = this.$props.timeslot?.beginn?.slice(0, 5);
+ },
+ async startEdit() {
+ if (this.$props.isTimeslotCardEditInProgress) {
+ this.$emit("setIsTimeslotCardEditInProgress", { value: false });
+ }
+ await this.$nextTick();
+ this.$emit("setIsTimeslotCardEditInProgress", { value: true });
+ await this.$nextTick();
+ this.isEditInProgress = true;
+ },
+ cancelEdit() {
+ this.isEditInProgress = false;
+ this.$emit("setIsTimeslotCardEditInProgress", { value: false });
+ },
+ saveChanges() {
+ if (!this.dateInput || !this.startTimeInput) {
+ window.alert("Check your inputs!");
+ return;
+ }
+
+ if (
+ this.dateInput === this.$props.timeslot?.datum &&
+ this.startTimeInput ===
+ this.$props.timeslot?.beginn?.slice(0, 5)
+ ) {
+ this.isEditInProgress = false;
+ return;
+ }
+
+ if (
+ this.startTimeInput < this.minStartTimeInput ||
+ this.startTimeInput > this.maxStartTimeInput
+ ) {
+ window.alert(
+ "Appointment start time must be between 07:00 and 22:55!",
+ );
+ return;
+ }
+
+ let minutes = parseInt(this.startTimeInput.slice(3));
+ if (minutes % 5) {
+ minutes = minutes - (minutes % 5);
+ this.startTimeInput =
+ this.startTimeInput.slice(0, 3) + numberPadding(minutes);
+ }
+
+ let startDate = new Date(
+ this.dateInput + " " + this.startTimeInput,
+ );
+ if (this.$props.timeslot) {
+ this.$emit("updateTimeslot", { newStartDate: startDate });
+ } else {
+ this.$emit("createTimeslot", { startDate });
+ }
+ },
+ },
+ created() {
+ this.setInputs();
+ },
+ template: /*html*/ `
+
+
+
+
+ {{ formattedDate }}
+ {{ formattedStartEnd }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+};
diff --git a/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyDurationSelector.js b/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyDurationSelector.js
index d54b80061..c1d0e5d34 100644
--- a/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyDurationSelector.js
+++ b/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyDurationSelector.js
@@ -57,7 +57,7 @@ export default {
},
template: /*html*/ `
-
+
{{ "* " + "Appointment duration (in minutes)" }}
+
+ {{ start }}
+ {{ end }}
+
+
+
+
+
+ `,
+};