diff --git a/application/config/Events.php b/application/config/Events.php index 80a8f03b3..76c25562f 100644 --- a/application/config/Events.php +++ b/application/config/Events.php @@ -33,5 +33,13 @@ Events::on('loadRenderers', function ($renderers) { ); }); +Events::on('loadRenderers', function ($renderers) { + $fhc_core_renderers =& $renderers(); + $fhc_core_renderers["coodle"] = array( + 'calendarEvent' => absoluteJsImportUrl('public/js/components/Cis/Renderer/Coodle/calendarEvent.js'), + 'calendarEventStyles' => APP_ROOT . 'public/css/Cis4/CoreCalendarEvents.css' + ); +}); + diff --git a/public/css/Cis4/Cis.css b/public/css/Cis4/Cis.css index 91807cd02..4ed1033e8 100644 --- a/public/css/Cis4/Cis.css +++ b/public/css/Cis4/Cis.css @@ -872,4 +872,12 @@ html { } .coodleParticipantSearchResult:hover { opacity: 80%; +} + +.coodleCalendarTimeslotCard:hover { + background-color: var(--fhc-background); +} + +.coodleCalendarTimeslotCardIcon:hover { + color: var(--fhc-primary); } \ No newline at end of file diff --git a/public/js/components/Calendar/Base.js b/public/js/components/Calendar/Base.js index 36d9877da..d02464f99 100644 --- a/public/js/components/Calendar/Base.js +++ b/public/js/components/Calendar/Base.js @@ -290,6 +290,7 @@ export default { :is="modes ? modes[cMode] : null || 'div'" ref="mode" v-model:current-date="cDate" + @emptyCellClicked="$emit('emptyCellClicked', $event)" @update:range="$emit('update:range', $event)" @request-modal-open="showEventModal" @request-modal-close="hideEventModal" diff --git a/public/js/components/Calendar/Base/Grid.js b/public/js/components/Calendar/Base/Grid.js index 3418a9151..3da35bd6c 100644 --- a/public/js/components/Calendar/Base/Grid.js +++ b/public/js/components/Calendar/Base/Grid.js @@ -399,7 +399,7 @@ export default { style="position:relative" :style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i" > - +
this.start.plus({ days: i })); + return Array.from({ length: 7 }, (e, i) => + this.start.plus({ days: i }), + ); }, axisParts() { if (this.timeGrid) { // create {start, end} array - return this.timeGrid.map(tu => { + return this.timeGrid.map((tu) => { return { start: luxon.Duration.fromISOTime(tu.start), - end: luxon.Duration.fromISOTime(tu.end) + end: luxon.Duration.fromISOTime(tu.end), }; }); } else { // create 07:00-23:00 - return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 })); + return Array.from({ length: 17 }, (e, i) => + luxon.Duration.fromObject({ hours: i + 7 }), + ); } - } + }, }, methods: { isToday(date) { - return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day'); - } + return date.hasSame( + luxon.DateTime.now().setZone(this.timezone), + "day", + ); + }, }, - template: /* html */` + template: /* html */ `
+
- ` -} + `, +}; diff --git a/public/js/components/Cis/Coodle/Components/CoodleSurvey.js b/public/js/components/Cis/Coodle/Components/CoodleSurvey.js index 92c1e0c0b..f6a59e987 100644 --- a/public/js/components/Cis/Coodle/Components/CoodleSurvey.js +++ b/public/js/components/Cis/Coodle/Components/CoodleSurvey.js @@ -25,7 +25,8 @@ export default { data() { return { surveyFormData: null, - isEditInProgress: false, + // todo: revert to default false + isEditInProgress: true, weekdays: [ "Sunday", "Monday", @@ -179,46 +180,10 @@ export default {
- - {{ "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*/ ` +
+
+ +
+ +
+
+
+ `, +}; 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 }} +
+
+ +
+
+ `, +};