mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 15:04:04 +00:00
implemented coodle survey calendar
This commit is contained in:
@@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -872,4 +872,12 @@ html {
|
||||
}
|
||||
.coodleParticipantSearchResult:hover {
|
||||
opacity: 80%;
|
||||
}
|
||||
|
||||
.coodleCalendarTimeslotCard:hover {
|
||||
background-color: var(--fhc-background);
|
||||
}
|
||||
|
||||
.coodleCalendarTimeslotCardIcon:hover {
|
||||
color: var(--fhc-primary);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -399,7 +399,7 @@ export default {
|
||||
style="position:relative"
|
||||
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
|
||||
>
|
||||
<slot name="part-body" v-bind="{ index, part }" />
|
||||
<slot name="part-body" v-bind="{ index, part, date }" />
|
||||
<div
|
||||
v-if="snapToGrid && dragging"
|
||||
style="position:absolute;inset:0;z-index:1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import CalendarGrid from '../../Base/Grid.js';
|
||||
import LabelDay from '../../Base/Label/Day.js';
|
||||
import LabelDow from '../../Base/Label/Dow.js';
|
||||
import LabelTime from '../../Base/Label/Time.js';
|
||||
import CalendarGrid from "../../Base/Grid.js";
|
||||
import LabelDay from "../../Base/Label/Day.js";
|
||||
import LabelDow from "../../Base/Label/Dow.js";
|
||||
import LabelTime from "../../Base/Label/Time.js";
|
||||
|
||||
export default {
|
||||
name: "WeekView",
|
||||
@@ -9,47 +9,54 @@ export default {
|
||||
CalendarGrid,
|
||||
LabelDay,
|
||||
LabelDow,
|
||||
LabelTime
|
||||
LabelTime,
|
||||
},
|
||||
inject: {
|
||||
timeGrid: "timeGrid",
|
||||
timezone: "timezone"
|
||||
timezone: "timezone",
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
collapseEmptyDays: Boolean
|
||||
collapseEmptyDays: Boolean,
|
||||
},
|
||||
computed: {
|
||||
start() {
|
||||
return this.day.startOf('week', { useLocaleWeeks: true });
|
||||
return this.day.startOf("week", { useLocaleWeeks: true });
|
||||
},
|
||||
axisMain() {
|
||||
return Array.from({ length: 7 }, (e, 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 */ `
|
||||
<div class="fhc-calendar-mode-week-view h-100">
|
||||
<calendar-grid
|
||||
ref="grid"
|
||||
@@ -73,6 +80,16 @@ export default {
|
||||
<template #part-header="{ part }">
|
||||
<label-time v-bind="{ part }" />
|
||||
</template>
|
||||
<template #part-body="params">
|
||||
<slot name="part-body">
|
||||
<div
|
||||
@click="($event) => {
|
||||
$event.params = params;
|
||||
$emit('emptyCellClicked', $event)
|
||||
}"
|
||||
class="position-absolute h-100 w-100" style="z-index:1"></div>
|
||||
</slot>
|
||||
</template>
|
||||
<template #event="slot">
|
||||
<div v-if="slot.event.type == 'loading'" class="placeholder-glow h-100 opacity-50">
|
||||
<span class="placeholder w-100 h-100" />
|
||||
@@ -81,5 +98,5 @@ export default {
|
||||
</template>
|
||||
</calendar-grid>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<span
|
||||
v-if="isEditInProgress"
|
||||
class="fst-italic"
|
||||
>
|
||||
{{ "Required inputs are marked with *." }}
|
||||
</span>
|
||||
<coodle-survey-basic-info
|
||||
v-model:surveyFormDataModelValue="surveyFormData"
|
||||
:survey="$props.survey"
|
||||
:parsedSelectedTimeslot="parsedSelectedTimeslot"
|
||||
:isEditInProgress="isEditInProgress"
|
||||
/>
|
||||
<hr>
|
||||
<div v-if="!isEditInProgress" class="d-flex flex-column gap-2">
|
||||
<coodle-survey-voting-table
|
||||
v-if="$props.survey"
|
||||
:uid="$props.uid"
|
||||
:timeslots="parsedTimeslotsForVotingTable"
|
||||
:survey="$props.survey"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="d-flex flex-column gap-3">
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl-4">
|
||||
<coodle-survey-participants
|
||||
v-if="surveyFormData?.participants"
|
||||
v-model:participantsModelValue="surveyFormData.participants"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<coodle-survey-duration-selector
|
||||
v-if="surveyFormData?.timeslotDuration"
|
||||
v-model:durationModelValue="surveyFormData.timeslotDuration"
|
||||
:survey="$props.survey"
|
||||
/>
|
||||
<coodle-survey-calendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isEditInProgress" class="d-flex flex-row justify-content-between">
|
||||
<span class="fst-italic">
|
||||
{{ "Required inputs are marked with *." }}
|
||||
</span>
|
||||
<div class="d-flex flex-row gap-2 justify-content-end align-items-start px-0">
|
||||
<div>
|
||||
<div
|
||||
@@ -241,6 +206,42 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<coodle-survey-basic-info
|
||||
v-model:surveyFormDataModelValue="surveyFormData"
|
||||
:survey="$props.survey"
|
||||
:parsedSelectedTimeslot="parsedSelectedTimeslot"
|
||||
:isEditInProgress="isEditInProgress"
|
||||
/>
|
||||
<hr>
|
||||
<div v-if="!isEditInProgress" class="d-flex flex-column gap-2">
|
||||
<coodle-survey-voting-table
|
||||
v-if="$props.survey"
|
||||
:uid="$props.uid"
|
||||
:timeslots="parsedTimeslotsForVotingTable"
|
||||
:survey="$props.survey"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="d-flex flex-column gap-3">
|
||||
<div class="row">
|
||||
<div class="col-12 col-xxl-4 pb-3">
|
||||
<coodle-survey-participants
|
||||
v-if="surveyFormData?.participants"
|
||||
v-model:participantsModelValue="surveyFormData.participants"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-xxl-8">
|
||||
<div class="d-flex flex-column gap-4">
|
||||
<coodle-survey-duration-selector
|
||||
v-if="surveyFormData?.timeslotDuration"
|
||||
v-model:durationModelValue="surveyFormData.timeslotDuration"
|
||||
:survey="$props.survey"
|
||||
/>
|
||||
<coodle-survey-calendar :surveyFormData="surveyFormData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
};
|
||||
|
||||
@@ -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*/ `
|
||||
<div>
|
||||
calendar placeholder
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="fw-bold">{{ "Timeslot selection" }}</span>
|
||||
<span class="fst-italic">{{ "You can add timeslots directly to the calendar or from the list down below." }}</span>
|
||||
</div>
|
||||
<div style="height:800px;">
|
||||
<fhc-calendar
|
||||
@emptyCellClicked="addTimeslotOnCalendarClick($event)"
|
||||
:ref="'coodleCalendar'"
|
||||
:timezone="timezone"
|
||||
:modes="modes"
|
||||
:modeOptions="modeOptions"
|
||||
:mode="'Week'"
|
||||
:timeGrid="null"
|
||||
:showBtns="false"
|
||||
:locale="$p.user_locale.value"
|
||||
:events="events"
|
||||
:backgrounds="backgrounds"
|
||||
:draggableEvents="true"
|
||||
:droppableEvents="true"
|
||||
>
|
||||
<template v-slot="{ event, mode }">
|
||||
<div
|
||||
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
|
||||
:type="mode == 'day' ? 'button' : undefined"
|
||||
>
|
||||
<component
|
||||
@deleteCoodleTimeslot="deleteTimeslot($event.timeslot)"
|
||||
:is="renderers[event.type]?.calendarEvent"
|
||||
:event="event"
|
||||
></component>
|
||||
</div>
|
||||
</template>
|
||||
</fhc-calendar>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div v-for="timeslot in timeslots.concat([null])" class="col-12 col-md-6 col-xl-4">
|
||||
<coodle-survey-calendar-timeslot-card
|
||||
@createTimeslot="createTimeslotFromCard($event.startDate)"
|
||||
@updateTimeslot="updateTimeslotFromCard(timeslot, $event.newStartDate)"
|
||||
@deleteTimeslot="deleteTimeslot(timeslot)"
|
||||
@setIsTimeslotCardEditInProgress="isTimeslotCardEditInProgress = $event.value"
|
||||
:timeslot="timeslot"
|
||||
:isTimeslotCardEditInProgress="isTimeslotCardEditInProgress"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
};
|
||||
|
||||
+192
@@ -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*/ `
|
||||
<div class="w-100 p-2" style="height: 120px;">
|
||||
<div class="coodleCalendarTimeslotCard h-100 d-flex flex-row justify-content-between px-3 py-2 border border-2 rounded-2">
|
||||
<template v-if="$props.timeslot || isEditInProgress">
|
||||
<div
|
||||
v-if="!isEditInProgress"
|
||||
class="d-flex flex-column gap-2 justify-content-center align-items-center flex-grow-1"
|
||||
>
|
||||
<span class="fs-5 fw-bold">{{ formattedDate }}</span>
|
||||
<span>{{ formattedStartEnd }}</span>
|
||||
</div>
|
||||
<div v-else class="d-flex flex-column gap-2 flex-grow-1 justify-content-center pe-3">
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label :for="'dateInput-' + $props.timeslot?.isostart" class="fw-bold">{{ "Date: " }}</label>
|
||||
<input v-model="dateInput" type="date" class="flex-grow-1" />
|
||||
</div>
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label :for="'startTimeInput-' + $props.timeslot?.isostart" class="fw-bold">{{ "Start time: " }}</label>
|
||||
<input
|
||||
v-model="startTimeInput"
|
||||
type="time"
|
||||
step="300"
|
||||
:min="minStartTimeInput"
|
||||
:max="maxStartTimeInput"
|
||||
class="flex-grow-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row align-items-start justify-content-end">
|
||||
<template v-if="!isEditInProgress">
|
||||
<div
|
||||
@click="startEdit()"
|
||||
:title="'Edit'"
|
||||
type="button"
|
||||
class="p-1 coodleCalendarTimeslotCardIcon"
|
||||
>
|
||||
<i class="fa-solid fa-pen-to-square fa-lg"></i>
|
||||
</div>
|
||||
<div
|
||||
@click="$emit('deleteTimeslot')"
|
||||
:title="'Delete'"
|
||||
type="button"
|
||||
class="p-1 coodleCalendarTimeslotCardIcon"
|
||||
>
|
||||
<i class="fa-solid fa-trash-can fa-lg"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
@click="cancelEdit()"
|
||||
:title="'Cancel'"
|
||||
type="button"
|
||||
class="p-1 coodleCalendarTimeslotCardIcon"
|
||||
>
|
||||
<i class="fa-regular fa-circle-left fa-lg"></i>
|
||||
</div>
|
||||
<div
|
||||
@click="saveChanges()"
|
||||
:title="'Save'"
|
||||
type="button"
|
||||
class="p-1 coodleCalendarTimeslotCardIcon"
|
||||
>
|
||||
<i class="fa-solid fa-floppy-disk fa-lg"></i>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex-grow-1 d-flex flex-row justify-content-center align-items-center">
|
||||
<i
|
||||
@click="startEdit()"
|
||||
:title="'Add'"
|
||||
type="button"
|
||||
class="coodleCalendarTimeslotCardIcon fa-solid fa-circle-plus fa-2xl"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
+1
-1
@@ -57,7 +57,7 @@ export default {
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<label class="fw-bold">{{ "* " + "Appointment duration (in minutes)" }}</label>
|
||||
<span class="fw-bold">{{ "* " + "Appointment duration (in minutes)" }}</span>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="d-flex flex-row align-items-center gap-2">
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
export default {
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["deleteCoodleTimeslot"],
|
||||
computed: {
|
||||
tooltipString() {
|
||||
const tooltipArray = [];
|
||||
|
||||
const dateFragments = this.$props.event.datum.split("-");
|
||||
tooltipArray.push(
|
||||
dateFragments[2] +
|
||||
"." +
|
||||
dateFragments[1] +
|
||||
"." +
|
||||
dateFragments[0] +
|
||||
".",
|
||||
);
|
||||
tooltipArray.push(this.start + "-" + this.end);
|
||||
|
||||
return tooltipArray.join("\n");
|
||||
},
|
||||
start() {
|
||||
return luxon.Duration.fromISOTime(this.event.beginn).toISOTime({
|
||||
suppressSeconds: true,
|
||||
});
|
||||
},
|
||||
end() {
|
||||
return luxon.Duration.fromISOTime(this.event.ende).toISOTime({
|
||||
suppressSeconds: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
template: /* html */ `
|
||||
<div
|
||||
@click.stop=""
|
||||
class="calendar-event-default h-100 w-100 p-1 d-flex flex-row"
|
||||
>
|
||||
<div
|
||||
v-if="event?.beginn && event?.ende"
|
||||
class="d-flex flex-column justify-content-center align-items-center flex-grow-1"
|
||||
>
|
||||
<span>{{ start }}</span>
|
||||
<span>{{ end }}</span>
|
||||
</div>
|
||||
<div
|
||||
@click="$emit('deleteCoodleTimeslot', { timeslot: $props.event })"
|
||||
class="d-flex justify-content-end align-items-start pt-2 pe-1"
|
||||
v-tooltip="tooltipString"
|
||||
>
|
||||
<i class="fa-solid fa-xmark fa-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
Reference in New Issue
Block a user