Compare commits
2 Commits
84867875c5
...
de04b53914
| Author | SHA1 | Date | |
|---|---|---|---|
|
de04b53914
|
|||
|
1a18881980
|
@@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.16.11",
|
"@quasar/extras": "^1.16.11",
|
||||||
|
"@quasar/quasar-ui-qcalendar": "^4.0.0-beta.19",
|
||||||
"appwrite": "^13.0.0",
|
"appwrite": "^13.0.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "3",
|
"vue": "3",
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^1.7.4",
|
"@quasar/app-vite": "^1.7.4",
|
||||||
"@quasar/quasar-app-extension-qcalendar": "^4.0.0-beta.15",
|
"@quasar/quasar-app-extension-qcalendar": "^4.0.0-beta.19",
|
||||||
"@types/node": "^12.20.21",
|
"@types/node": "^12.20.21",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
|
|||||||
@@ -1,86 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-card-section style="max-width: 320px">
|
<q-banner :class="$q.dark.isActive ? 'bg-grey-9' : 'bg-grey-3'">
|
||||||
<div class="text-caption">
|
Use the calendar to pick a date. Select an available boat and timeslot
|
||||||
Use the calendar to pick a date. Select an available boat and timeslot
|
below.
|
||||||
below.
|
</q-banner>
|
||||||
</div>
|
<BoatScheduleTableComponent />
|
||||||
<div
|
|
||||||
style="
|
|
||||||
width: 100%;
|
|
||||||
max-width: 320px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
width: 50%;
|
|
||||||
max-width: 350px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="q-button"
|
|
||||||
style="cursor: pointer; user-select: none"
|
|
||||||
@click="onPrev"
|
|
||||||
><</span
|
|
||||||
>
|
|
||||||
{{ formattedMonth }}
|
|
||||||
<span
|
|
||||||
class="q-button"
|
|
||||||
style="cursor: pointer; user-select: none"
|
|
||||||
@click="onNext"
|
|
||||||
>></span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div style="display: flex; width: 100%">
|
|
||||||
<q-calendar-month
|
|
||||||
ref="calendar"
|
|
||||||
v-model="selectedDate"
|
|
||||||
:disabled-before="disabledBefore"
|
|
||||||
:disabled-days="disabledDays()"
|
|
||||||
animated
|
|
||||||
bordered
|
|
||||||
mini-mode
|
|
||||||
date-type="rounded"
|
|
||||||
@click-date="onClickDate"
|
|
||||||
@change="onChange"
|
|
||||||
/>
|
|
||||||
</div></div
|
|
||||||
></q-card-section>
|
|
||||||
<q-card-section style="max-width: 320px">
|
|
||||||
<div v-for="boat in boatStore.boats" :key="boat.name">
|
|
||||||
<q-item-label header>{{ boat.name }}</q-item-label>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-option-group
|
|
||||||
:options="boatoptions(boat)"
|
|
||||||
type="radio"
|
|
||||||
v-model="selectedBoatTime"
|
|
||||||
>
|
|
||||||
<template v-slot:label="opt">
|
|
||||||
<div class="row items-center">
|
|
||||||
{{ opt.label }}
|
|
||||||
<span class="text-caption" v-if="opt.disable"
|
|
||||||
>Reserved by {{ opt.user }}</span
|
|
||||||
>
|
|
||||||
</div></template
|
|
||||||
>
|
|
||||||
</q-option-group>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -95,6 +18,7 @@ import { Boat, useBoatStore } from 'src/stores/boat';
|
|||||||
import { useScheduleStore, Timeblock } from 'src/stores/schedule';
|
import { useScheduleStore, Timeblock } from 'src/stores/schedule';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
|
import BoatScheduleTableComponent from './boat/BoatScheduleTableComponent.vue';
|
||||||
|
|
||||||
type EventData = {
|
type EventData = {
|
||||||
event: object;
|
event: object;
|
||||||
@@ -107,17 +31,10 @@ type EventData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const calendar = ref();
|
const calendar = ref();
|
||||||
const boatStore = useBoatStore();
|
|
||||||
const scheduleStore = useScheduleStore();
|
const scheduleStore = useScheduleStore();
|
||||||
const selectedDate = ref(today());
|
const selectedDate = ref(today());
|
||||||
const selectedBoatTime = ref();
|
const selectedBoatTime = ref();
|
||||||
|
|
||||||
const formattedMonth = computed(() => {
|
|
||||||
const date = new Date(selectedDate.value);
|
|
||||||
|
|
||||||
return monthFormatter()?.format(date);
|
|
||||||
});
|
|
||||||
|
|
||||||
const disabledBefore = computed(() => {
|
const disabledBefore = computed(() => {
|
||||||
const todayTs = parseTimestamp(today()) as Timestamp;
|
const todayTs = parseTimestamp(today()) as Timestamp;
|
||||||
return addToDate(todayTs, { day: -1 }).date;
|
return addToDate(todayTs, { day: -1 }).date;
|
||||||
|
|||||||
118
src/components/scheduling/boat/BoatScheduleTableComponent.vue
Normal file
118
src/components/scheduling/boat/BoatScheduleTableComponent.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-px-sm">
|
||||||
|
<CalendarHeaderComponent v-model="selectedDate" />
|
||||||
|
<div class="boat-schedule-table-component">
|
||||||
|
<QCalendarDay
|
||||||
|
ref="calendar"
|
||||||
|
class="q-pt-xs"
|
||||||
|
flat
|
||||||
|
animated
|
||||||
|
dense
|
||||||
|
v-model="selectedDate"
|
||||||
|
:column-count="boats.length"
|
||||||
|
@change="scrollToEvent()"
|
||||||
|
>
|
||||||
|
<template #head-day="{ scope }">
|
||||||
|
<div style="text-align: center; font-weight: 800">
|
||||||
|
{{ boats[scope.columnIndex].displayName }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #day-body="{ scope }">
|
||||||
|
<div
|
||||||
|
v-for="block in scheduleStore.getTimeblocksForDate(scope.timestamp)"
|
||||||
|
:key="block.id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="timeblock"
|
||||||
|
:style="
|
||||||
|
blockStyles(block, scope.timeStartPos, scope.timeDurationHeight)
|
||||||
|
"
|
||||||
|
@click="selectBlock($event, scope, block)"
|
||||||
|
>
|
||||||
|
Available
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</QCalendarDay>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Timestamp, diffTimestamp, today } from '@quasar/quasar-ui-qcalendar';
|
||||||
|
|
||||||
|
import CalendarHeaderComponent from './CalendarHeaderComponent.vue';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useBoatStore } from 'src/stores/boat';
|
||||||
|
import { Timeblock, useScheduleStore } from 'src/stores/schedule';
|
||||||
|
|
||||||
|
const scheduleStore = useScheduleStore();
|
||||||
|
const boatStore = useBoatStore();
|
||||||
|
|
||||||
|
const selectedDate = ref(today());
|
||||||
|
|
||||||
|
const boats = boatStore.boats;
|
||||||
|
|
||||||
|
const calendar = ref<QCalendarDay.QCalendarDay | null>(null);
|
||||||
|
|
||||||
|
function blockStyles(
|
||||||
|
block: Timeblock,
|
||||||
|
timeStartPos: (t: string) => string,
|
||||||
|
timeDurationHeight: (d: number) => string
|
||||||
|
) {
|
||||||
|
const s = {
|
||||||
|
top: '',
|
||||||
|
height: '',
|
||||||
|
};
|
||||||
|
if (timeStartPos && timeDurationHeight) {
|
||||||
|
s.top = timeStartPos(block.start.time) + 'px';
|
||||||
|
s.height =
|
||||||
|
timeDurationHeight(
|
||||||
|
diffTimestamp(block.start, block.end, false) / 1000 / 60
|
||||||
|
) + 'px';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DayBodyScope {
|
||||||
|
columnIndex: number;
|
||||||
|
timeDurationHeight: string;
|
||||||
|
timeStartPos: (time: string, clamp: boolean) => string;
|
||||||
|
timestamp: Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Timeblock) {
|
||||||
|
const target = event.target as HTMLDivElement;
|
||||||
|
target.classList.add('selected');
|
||||||
|
target.textContent = 'selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToEvent() {
|
||||||
|
setTimeout(() => calendar.value?.scrollToTime('09:00'), 0); // Should figure out why we need this setTimeout...
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
.boat-schedule-table-component
|
||||||
|
display: flex
|
||||||
|
height: 40vh
|
||||||
|
.timeblock
|
||||||
|
display: flex
|
||||||
|
position: absolute
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
width: 100%
|
||||||
|
opacity: 0.25
|
||||||
|
margin: 0 1px
|
||||||
|
text-overflow: ellipsis
|
||||||
|
overflow: hidden
|
||||||
|
font-size: 0.8em
|
||||||
|
cursor: pointer
|
||||||
|
background: $primary
|
||||||
|
color: white
|
||||||
|
border: 2px solid black
|
||||||
|
.selected
|
||||||
|
opacity: 1 !important
|
||||||
|
</style>
|
||||||
241
src/components/scheduling/boat/CalendarHeaderComponent.vue
Normal file
241
src/components/scheduling/boat/CalendarHeaderComponent.vue
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<div class="title-bar" style="display: flex">
|
||||||
|
<button
|
||||||
|
tabindex="0"
|
||||||
|
class="date-button direction-button direction-button__left"
|
||||||
|
@click="onPrev"
|
||||||
|
>
|
||||||
|
<span class="q-calendar__focus-helper" tabindex="-1" />
|
||||||
|
</button>
|
||||||
|
<div class="dates-holder">
|
||||||
|
<div :key="parsedStart?.date" class="internal-dates-holder">
|
||||||
|
<div v-for="day in days" :key="day.date" :style="dayStyle">
|
||||||
|
<button
|
||||||
|
tabindex="0"
|
||||||
|
style="width: 100%"
|
||||||
|
:class="dayClass(day)"
|
||||||
|
@click="selectedDate = day.date"
|
||||||
|
>
|
||||||
|
<span class="q-calendar__focus-helper" tabindex="-1" />
|
||||||
|
<div style="width: 100%">
|
||||||
|
{{ monthFormatter(day, true) }}
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%; font-size: 16px; font-weight: 700">
|
||||||
|
{{ dayFormatter(day, false) }}
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%; font-size: 10px">
|
||||||
|
{{ weekdayFormatter(day, true) }}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
tabindex="0"
|
||||||
|
class="date-button direction-button direction-button__right"
|
||||||
|
@click="onNext"
|
||||||
|
>
|
||||||
|
<span class="q-calendar__focus-helper" tabindex="-1" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Timestamp,
|
||||||
|
addToDate,
|
||||||
|
createDayList,
|
||||||
|
createNativeLocaleFormatter,
|
||||||
|
getEndOfWeek,
|
||||||
|
getStartOfWeek,
|
||||||
|
getWeekdaySkips,
|
||||||
|
parseTimestamp,
|
||||||
|
today,
|
||||||
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
|
|
||||||
|
import { ref, reactive, computed } from 'vue';
|
||||||
|
|
||||||
|
const selectedDate = defineModel<string>();
|
||||||
|
|
||||||
|
const weekdays = reactive([0, 1, 2, 3, 4, 5, 6]),
|
||||||
|
locale = ref('en-CA'),
|
||||||
|
monthFormatter = monthFormatterFunc(),
|
||||||
|
dayFormatter = dayFormatterFunc(),
|
||||||
|
weekdayFormatter = weekdayFormatterFunc();
|
||||||
|
|
||||||
|
const weekdaySkips = computed(() => {
|
||||||
|
return getWeekdaySkips(weekdays);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedStart = computed(() =>
|
||||||
|
getStartOfWeek(
|
||||||
|
parseTimestamp(selectedDate.value || today()) as Timestamp,
|
||||||
|
weekdays,
|
||||||
|
today2.value as Timestamp
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedEnd = computed(() =>
|
||||||
|
getEndOfWeek(
|
||||||
|
parseTimestamp(selectedDate.value || today()) as Timestamp,
|
||||||
|
weekdays,
|
||||||
|
today2.value as Timestamp
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const today2 = computed(() => {
|
||||||
|
return parseTimestamp(today());
|
||||||
|
});
|
||||||
|
|
||||||
|
const days = computed(() => {
|
||||||
|
if (parsedStart.value && parsedEnd.value) {
|
||||||
|
return createDayList(
|
||||||
|
parsedStart.value,
|
||||||
|
parsedEnd.value,
|
||||||
|
today2.value as Timestamp,
|
||||||
|
weekdaySkips.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const dayStyle = computed(() => {
|
||||||
|
const width = 100 / weekdays.length + '%';
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function onPrev() {
|
||||||
|
const ts = addToDate(parsedStart.value, { day: -7 });
|
||||||
|
selectedDate.value = ts.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNext() {
|
||||||
|
const ts = addToDate(parsedStart.value, { day: 7 });
|
||||||
|
selectedDate.value = ts.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayClass(day: Timestamp) {
|
||||||
|
return {
|
||||||
|
'date-button': true,
|
||||||
|
'selected-date-button': selectedDate.value === day.date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthFormatterFunc() {
|
||||||
|
const longOptions = { timeZone: 'UTC', month: 'long' };
|
||||||
|
const shortOptions = { timeZone: 'UTC', month: 'short' };
|
||||||
|
|
||||||
|
return createNativeLocaleFormatter(locale.value, (_tms, short) =>
|
||||||
|
short ? shortOptions : longOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function weekdayFormatterFunc() {
|
||||||
|
const longOptions = { timeZone: 'UTC', weekday: 'long' };
|
||||||
|
const shortOptions = { timeZone: 'UTC', weekday: 'short' };
|
||||||
|
|
||||||
|
return createNativeLocaleFormatter(locale.value, (_tms, short) =>
|
||||||
|
short ? shortOptions : longOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayFormatterFunc() {
|
||||||
|
const longOptions = { timeZone: 'UTC', day: '2-digit' };
|
||||||
|
const shortOptions = { timeZone: 'UTC', day: 'numeric' };
|
||||||
|
|
||||||
|
return createNativeLocaleFormatter(locale.value, (_tms, short) =>
|
||||||
|
short ? shortOptions : longOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="sass">
|
||||||
|
.title-bar
|
||||||
|
position: relative
|
||||||
|
width: 100%
|
||||||
|
height: 70px
|
||||||
|
background: $primary
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex: 1 0 100%
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
overflow: hidden
|
||||||
|
border-radius: 3px
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
.dates-holder
|
||||||
|
position: relative
|
||||||
|
width: 100%
|
||||||
|
align-items: center
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
color: #fff
|
||||||
|
overflow: hidden
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
.internal-dates-holder
|
||||||
|
position: relative
|
||||||
|
width: 100%
|
||||||
|
display: inline-flex
|
||||||
|
flex: 1 1 100%
|
||||||
|
flex-direction: row
|
||||||
|
justify-content: space-between
|
||||||
|
overflow: hidden
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
.direction-button
|
||||||
|
background: $primary
|
||||||
|
color: white
|
||||||
|
width: 40px
|
||||||
|
max-width: 50px !important
|
||||||
|
|
||||||
|
.direction-button__left
|
||||||
|
&:before
|
||||||
|
content: '<'
|
||||||
|
display: inline-flex
|
||||||
|
flex-direction: column
|
||||||
|
justify-content: center
|
||||||
|
height: 100%
|
||||||
|
font-weight: 900
|
||||||
|
font-size: 3em
|
||||||
|
|
||||||
|
.direction-button__right
|
||||||
|
&:before
|
||||||
|
content: '>'
|
||||||
|
display: inline-flex
|
||||||
|
flex-direction: column
|
||||||
|
justify-content: center
|
||||||
|
height: 100%
|
||||||
|
font-weight: 900
|
||||||
|
font-size: 3em
|
||||||
|
|
||||||
|
.date-button
|
||||||
|
color: white
|
||||||
|
background: $primary
|
||||||
|
z-index: 2
|
||||||
|
height: 100%
|
||||||
|
outline: 0
|
||||||
|
cursor: pointer
|
||||||
|
border-radius: 3px
|
||||||
|
display: inline-flex
|
||||||
|
flex: 1 0 auto
|
||||||
|
flex-direction: column
|
||||||
|
align-items: stretch
|
||||||
|
position: relative
|
||||||
|
border: 0
|
||||||
|
vertical-align: middle
|
||||||
|
padding: 0
|
||||||
|
font-size: 14px
|
||||||
|
line-height: 1.715em
|
||||||
|
text-decoration: none
|
||||||
|
font-weight: 500
|
||||||
|
text-transform: uppercase
|
||||||
|
text-align: center
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
.selected-date-button
|
||||||
|
color: #3f51b5 !important
|
||||||
|
background: white !important
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page>
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
|
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-sm">
|
||||||
<q-input
|
<q-input
|
||||||
bottom-slots
|
bottom-slots
|
||||||
v-model="bookingForm.name"
|
v-model="bookingForm.name"
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, computed, watch } from 'vue';
|
import { reactive, ref, computed, watch } from 'vue';
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
import { Boat } from 'src/stores/boat';
|
||||||
import { Dialog, date } from 'quasar';
|
import { Dialog, date } from 'quasar';
|
||||||
import BoatSelection from 'src/components/scheduling/BoatSelection.vue';
|
import BoatSelection from 'src/components/scheduling/BoatSelection.vue';
|
||||||
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';
|
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';
|
||||||
|
|||||||
@@ -4,56 +4,65 @@
|
|||||||
<!-- <navigation-bar @today="onToday" @prev="onPrev" @next="onNext" /> -->
|
<!-- <navigation-bar @today="onToday" @prev="onPrev" @next="onNext" /> -->
|
||||||
|
|
||||||
<div class="row justify-center">
|
<div class="row justify-center">
|
||||||
<div
|
<q-calendar-day
|
||||||
style="display: flex; max-width: 800px; width: 100%; height: 400px"
|
ref="calendar"
|
||||||
|
v-model="selectedDate"
|
||||||
|
view="day"
|
||||||
|
:max-days="3"
|
||||||
|
bordered
|
||||||
|
animated
|
||||||
|
transition-next="slide-left"
|
||||||
|
transition-prev="slide-right"
|
||||||
|
@change="onChange"
|
||||||
|
@moved="onMoved"
|
||||||
|
@click-date="onClickDate"
|
||||||
|
@click-time="onClickTime"
|
||||||
|
@click-interval="onClickInterval"
|
||||||
|
@click-head-day="onClickHeadDay"
|
||||||
>
|
>
|
||||||
<q-calendar-day
|
<template
|
||||||
ref="calendar"
|
#day-body="{
|
||||||
v-model="selectedDate"
|
scope: { timestamp, timeStartPos, timeDurationHeight },
|
||||||
view="day"
|
}"
|
||||||
:max-days="3"
|
|
||||||
bordered
|
|
||||||
animated
|
|
||||||
transition-next="slide-left"
|
|
||||||
transition-prev="slide-right"
|
|
||||||
@change="onChange"
|
|
||||||
@moved="onMoved"
|
|
||||||
@click-date="onClickDate"
|
|
||||||
@click-time="onClickTime"
|
|
||||||
@click-interval="onClickInterval"
|
|
||||||
@click-head-intervals="onClickHeadIntervals"
|
|
||||||
@click-head-day="onClickHeadDay"
|
|
||||||
>
|
>
|
||||||
<template #day-body="{ scope: { timestamp } }">
|
<template
|
||||||
<template
|
v-for="event in reservationEvents(timestamp)"
|
||||||
v-for="event in scheduleStore.getBoatReservations(
|
:key="event.id"
|
||||||
makeDateTime(timestamp)
|
>
|
||||||
)"
|
<div
|
||||||
:key="event.id"
|
v-if="event.start !== undefined"
|
||||||
|
class="booking-event"
|
||||||
|
:style="slotStyle(event, timeStartPos, timeDurationHeight)"
|
||||||
>
|
>
|
||||||
{{ timestamp }}
|
<span class="title q-calendar__ellipsis">
|
||||||
<div v-if="event.start !== undefined" class="my-event">
|
{{ event.user }}
|
||||||
<div class="title q-calendar__ellipsis">
|
<q-tooltip>{{
|
||||||
{{ event.user }}
|
event.start + ' - ' + event.resource.name
|
||||||
<q-tooltip>{{
|
}}</q-tooltip>
|
||||||
event.start + ' - ' + event.resource.name
|
</span>
|
||||||
}}</q-tooltip>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</q-calendar-day>
|
</template>
|
||||||
</div>
|
</q-calendar-day>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useScheduleStore } from 'src/stores/schedule';
|
import { Reservation, useScheduleStore } from 'src/stores/schedule';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
const scheduleStore = useScheduleStore();
|
const scheduleStore = useScheduleStore();
|
||||||
import { QCalendarDay, today, makeDateTime } from '@quasar/quasar-ui-qcalendar';
|
import {
|
||||||
|
TimestampOrNull,
|
||||||
|
makeDateTime,
|
||||||
|
makeDate,
|
||||||
|
parseDate,
|
||||||
|
today,
|
||||||
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
|
import { QCalendarDay } from '@quasar/quasar-ui-qcalendar';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
||||||
|
|
||||||
const selectedDate = ref(today());
|
const selectedDate = ref(today());
|
||||||
|
|
||||||
@@ -61,6 +70,30 @@ const selectedDate = ref(today());
|
|||||||
const calendarRef = ref(QCalendarDay);
|
const calendarRef = ref(QCalendarDay);
|
||||||
|
|
||||||
// Method declarations
|
// Method declarations
|
||||||
|
|
||||||
|
function slotStyle(
|
||||||
|
event: Reservation,
|
||||||
|
timeStartPos: (time: TimestampOrNull) => string,
|
||||||
|
timeDurationHeight: (minutes: number) => string
|
||||||
|
) {
|
||||||
|
const s = {
|
||||||
|
top: '',
|
||||||
|
height: '',
|
||||||
|
'align-items': 'flex-start',
|
||||||
|
};
|
||||||
|
if (timeStartPos && timeDurationHeight) {
|
||||||
|
s.top = timeStartPos(parseDate(event.start)) + 'px';
|
||||||
|
s.height =
|
||||||
|
timeDurationHeight(date.getDateDiff(event.end, event.start, 'minutes')) +
|
||||||
|
'px';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reservationEvents(timestamp: Timestamp) {
|
||||||
|
return scheduleStore.getBoatReservations(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
function onToday() {
|
function onToday() {
|
||||||
calendarRef.value.moveToToday();
|
calendarRef.value.moveToToday();
|
||||||
}
|
}
|
||||||
@@ -85,10 +118,28 @@ function onClickTime(data) {
|
|||||||
function onClickInterval(data) {
|
function onClickInterval(data) {
|
||||||
console.log('onClickInterval', data);
|
console.log('onClickInterval', data);
|
||||||
}
|
}
|
||||||
function onClickHeadIntervals(data) {
|
|
||||||
console.log('onClickHeadIntervals', data);
|
|
||||||
}
|
|
||||||
function onClickHeadDay(data) {
|
function onClickHeadDay(data) {
|
||||||
console.log('onClickHeadDay', data);
|
console.log('onClickHeadDay', data);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.booking-event
|
||||||
|
position: absolute
|
||||||
|
font-size: 12px
|
||||||
|
justify-content: space-evenly
|
||||||
|
margin: 0 1px
|
||||||
|
text-overflow: ellipsis
|
||||||
|
overflow: hidden
|
||||||
|
color: white
|
||||||
|
max-width: 100%
|
||||||
|
background: #027BE3FF
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
.title
|
||||||
|
position: relative
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
height: 100%
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ and rough engine performance.`,
|
|||||||
year: 1989,
|
year: 1989,
|
||||||
imgsrc: '/tmpimg/capri25.png',
|
imgsrc: '/tmpimg/capri25.png',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
$id: '3',
|
||||||
|
name: 'Just My Imagination',
|
||||||
|
displayName: 'JMI',
|
||||||
|
class: 'Capri 25',
|
||||||
|
year: 1989,
|
||||||
|
imgsrc: '/tmpimg/capri25.png',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const useBoatStore = defineStore('boat', {
|
export const useBoatStore = defineStore('boat', {
|
||||||
|
|||||||
@@ -3,8 +3,13 @@ import { ref } from 'vue';
|
|||||||
import { Boat, useBoatStore } from './boat';
|
import { Boat, useBoatStore } from './boat';
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { DateOptions } from 'quasar';
|
import { DateOptions } from 'quasar';
|
||||||
import { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
import {
|
||||||
import { timeStamp } from 'console';
|
Timestamp,
|
||||||
|
parseDate,
|
||||||
|
updateRelative,
|
||||||
|
today,
|
||||||
|
parsed,
|
||||||
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
|
|
||||||
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
||||||
export type Reservation = {
|
export type Reservation = {
|
||||||
@@ -16,32 +21,38 @@ export type Reservation = {
|
|||||||
reservationDate: Date;
|
reservationDate: Date;
|
||||||
status?: StatusTypes;
|
status?: StatusTypes;
|
||||||
};
|
};
|
||||||
|
// 24 hrs in advance only 2 weekday, and 1 weekend slot
|
||||||
|
// Within 24 hrs, any available slot
|
||||||
/* TODO: Figure out how best to separate out where qcalendar bits should be.
|
/* TODO: Figure out how best to separate out where qcalendar bits should be.
|
||||||
e.g.: Should there be any qcalendar stuff in this store? Or should we have just JS Date
|
e.g.: Should there be any qcalendar stuff in this store? Or should we have just JS Date
|
||||||
objects in here? */
|
objects in here? */
|
||||||
|
|
||||||
export type Timeblock = {
|
export type Timeblock = {
|
||||||
|
id: number;
|
||||||
start: Timestamp;
|
start: Timestamp;
|
||||||
end: Timestamp;
|
end: Timestamp;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sampleBlocks = [
|
const sampleBlocks = [
|
||||||
{
|
{
|
||||||
start: { time: '09:00', hour: 9, minute: 0, hasDay: false, hasTime: true },
|
id: 1,
|
||||||
end: { time: '12:00', hour: 12, minute: 0, hasDay: false, hasTime: true },
|
start: { time: '09:00', hour: 9, minute: 0, hasTime: true },
|
||||||
|
end: { time: '11:30', hour: 12, minute: 0, hasTime: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
start: { time: '12:00', hour: 12, minute: 0, hasDay: false, hasTime: true },
|
id: 2,
|
||||||
end: { time: '15:00', hour: 15, minute: 0, hasDay: false, hasTime: true },
|
start: { time: '12:00', hour: 12, minute: 0, hasTime: true },
|
||||||
|
end: { time: '15:00', hour: 15, minute: 0, hasTime: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
start: { time: '15:00', hour: 15, minute: 0, hasDay: false, hasTime: true },
|
id: 3,
|
||||||
end: { time: '18:00', hour: 18, minute: 0, hasDay: false, hasTime: true },
|
start: { time: '15:00', hour: 15, minute: 0, hasTime: true },
|
||||||
|
end: { time: '18:00', hour: 18, minute: 0, hasTime: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
start: { time: '18:00', hour: 18, minute: 0, hasDay: false, hasTime: true },
|
id: 4,
|
||||||
end: { time: '21:00', hour: 21, minute: 0, hasDay: false, hasTime: true },
|
start: { time: '18:00', hour: 18, minute: 0, hasTime: true },
|
||||||
|
end: { time: '21:00', hour: 21, minute: 0, hasTime: true },
|
||||||
},
|
},
|
||||||
] as Timeblock[];
|
] as Timeblock[];
|
||||||
|
|
||||||
@@ -128,16 +139,26 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
const reservations = ref<Reservation[]>(getSampleReservations());
|
const reservations = ref<Reservation[]>(getSampleReservations());
|
||||||
const timeblocks = sampleBlocks;
|
const timeblocks = sampleBlocks;
|
||||||
|
|
||||||
const getTimeblocksForDate = (date: Date): Timeblock[] => timeblocks;
|
const getTimeblocksForDate = (date: Timestamp): Timeblock[] => {
|
||||||
|
return timeblocks.map((t) => {
|
||||||
|
return {
|
||||||
|
// Update all the start/end times with the passed in date
|
||||||
|
...t,
|
||||||
|
start: { ...date, ...t.start },
|
||||||
|
end: { ...date, ...t.end },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getBoatReservations = (
|
const getBoatReservations = (
|
||||||
searchDate: Date,
|
searchDate: Timestamp,
|
||||||
boat?: string
|
boat?: string
|
||||||
): Reservation[] => {
|
): Reservation[] => {
|
||||||
return reservations.value.filter((x) => {
|
return reservations.value.filter((x) => {
|
||||||
|
console.log(searchDate);
|
||||||
return (
|
return (
|
||||||
((x.start.getDate() == searchDate.getDate() ||
|
((parseDate(x.start)?.date == searchDate.date ||
|
||||||
x.end.getDate() == searchDate.getDate()) && // Part of reservation falls on day
|
parseDate(x.end)?.date == searchDate.date) && // Part of reservation falls on day
|
||||||
x.resource != undefined && // A boat is defined
|
x.resource != undefined && // A boat is defined
|
||||||
!boat) ||
|
!boat) ||
|
||||||
x.resource.$id == boat // A specific boat has been passed, and matches
|
x.resource.$id == boat // A specific boat has been passed, and matches
|
||||||
|
|||||||
11
yarn.lock
11
yarn.lock
@@ -1120,18 +1120,23 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.11.tgz#84b1efb9097a6e58c3ebfdd5da83ac658056a35c"
|
resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.11.tgz#84b1efb9097a6e58c3ebfdd5da83ac658056a35c"
|
||||||
integrity sha512-sbTBHOA+Hi7ah0P6qSm+xfRXqwJ94ct3NKA3Lkq3iNPYuHD7VXbSWtP2eA7Cu9Fd0WjVoPbngf6yFGg46U3IfQ==
|
integrity sha512-sbTBHOA+Hi7ah0P6qSm+xfRXqwJ94ct3NKA3Lkq3iNPYuHD7VXbSWtP2eA7Cu9Fd0WjVoPbngf6yFGg46U3IfQ==
|
||||||
|
|
||||||
"@quasar/quasar-app-extension-qcalendar@^4.0.0-beta.15":
|
"@quasar/quasar-app-extension-qcalendar@^4.0.0-beta.19":
|
||||||
version "4.0.0-beta.16"
|
version "4.0.0-beta.16"
|
||||||
resolved "https://registry.npmjs.org/@quasar/quasar-app-extension-qcalendar/-/quasar-app-extension-qcalendar-4.0.0-beta.16.tgz"
|
resolved "https://registry.yarnpkg.com/@quasar/quasar-app-extension-qcalendar/-/quasar-app-extension-qcalendar-4.0.0-beta.16.tgz#5b0bdfb04db59cf6017892ff91563dd9759c7bb5"
|
||||||
integrity sha512-Rj3KKjPFrE13cswlZAPcqdqi1YH9CeHMpWIw8xsNqdLhCoaRhMGbRas9fvHFLJOXpnsDaVwWINNgN/bBUyn99w==
|
integrity sha512-Rj3KKjPFrE13cswlZAPcqdqi1YH9CeHMpWIw8xsNqdLhCoaRhMGbRas9fvHFLJOXpnsDaVwWINNgN/bBUyn99w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@quasar/quasar-ui-qcalendar" "^4.0.0-beta.16"
|
"@quasar/quasar-ui-qcalendar" "^4.0.0-beta.16"
|
||||||
|
|
||||||
"@quasar/quasar-ui-qcalendar@^4.0.0-beta.16":
|
"@quasar/quasar-ui-qcalendar@^4.0.0-beta.16":
|
||||||
version "4.0.0-beta.16"
|
version "4.0.0-beta.16"
|
||||||
resolved "https://registry.npmjs.org/@quasar/quasar-ui-qcalendar/-/quasar-ui-qcalendar-4.0.0-beta.16.tgz"
|
resolved "https://registry.yarnpkg.com/@quasar/quasar-ui-qcalendar/-/quasar-ui-qcalendar-4.0.0-beta.16.tgz#90dca0962f1fe1068361f387893df6c5da7522e2"
|
||||||
integrity sha512-KVbFJD1HQp91tiklv+6XsG7bq8FKK6mhhnoVzmjgoyhUAEb9csfbDPbpegy1/FzXy3o0wITe6mmRZ8nbaiMEZg==
|
integrity sha512-KVbFJD1HQp91tiklv+6XsG7bq8FKK6mhhnoVzmjgoyhUAEb9csfbDPbpegy1/FzXy3o0wITe6mmRZ8nbaiMEZg==
|
||||||
|
|
||||||
|
"@quasar/quasar-ui-qcalendar@^4.0.0-beta.19":
|
||||||
|
version "4.0.0-beta.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/@quasar/quasar-ui-qcalendar/-/quasar-ui-qcalendar-4.0.0-beta.19.tgz#11229ad28f65991c2de584faf2e0196563a0e633"
|
||||||
|
integrity sha512-BT0G2JjgKl1bqNrY5utcYeoy8gK+U9k3Pz1YDi1OB265W/jHU6nFoWMEUdY3JdvMccwkXTL2DLVyl3eqAUyLyg==
|
||||||
|
|
||||||
"@quasar/render-ssr-error@^1.0.3":
|
"@quasar/render-ssr-error@^1.0.3":
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@quasar/render-ssr-error/-/render-ssr-error-1.0.3.tgz#33f27231007d1b222de41d3d70c29a6d14f9498a"
|
resolved "https://registry.yarnpkg.com/@quasar/render-ssr-error/-/render-ssr-error-1.0.3.tgz#33f27231007d1b222de41d3d70c29a6d14f9498a"
|
||||||
|
|||||||
Reference in New Issue
Block a user