Compare commits

2 Commits

Author SHA1 Message Date
de04b53914 Time Select
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 38s
2024-04-28 19:07:00 -04:00
1a18881980 Update QCalendar 2024-04-13 21:01:42 -04:00
9 changed files with 514 additions and 152 deletions

View File

@@ -14,6 +14,7 @@
},
"dependencies": {
"@quasar/extras": "^1.16.11",
"@quasar/quasar-ui-qcalendar": "^4.0.0-beta.19",
"appwrite": "^13.0.0",
"pinia": "^2.1.7",
"vue": "3",
@@ -21,7 +22,7 @@
},
"devDependencies": {
"@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",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",

View File

@@ -1,86 +1,9 @@
<template>
<q-card-section style="max-width: 320px">
<div class="text-caption">
Use the calendar to pick a date. Select an available boat and timeslot
below.
</div>
<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"
>&lt;</span
>
{{ formattedMonth }}
<span
class="q-button"
style="cursor: pointer; user-select: none"
@click="onNext"
>&gt;</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 }} &nbsp;
<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>
<q-banner :class="$q.dark.isActive ? 'bg-grey-9' : 'bg-grey-3'">
Use the calendar to pick a date. Select an available boat and timeslot
below.
</q-banner>
<BoatScheduleTableComponent />
</template>
<script setup lang="ts">
@@ -95,6 +18,7 @@ import { Boat, useBoatStore } from 'src/stores/boat';
import { useScheduleStore, Timeblock } from 'src/stores/schedule';
import { computed } from 'vue';
import { date } from 'quasar';
import BoatScheduleTableComponent from './boat/BoatScheduleTableComponent.vue';
type EventData = {
event: object;
@@ -107,17 +31,10 @@ type EventData = {
};
const calendar = ref();
const boatStore = useBoatStore();
const scheduleStore = useScheduleStore();
const selectedDate = ref(today());
const selectedBoatTime = ref();
const formattedMonth = computed(() => {
const date = new Date(selectedDate.value);
return monthFormatter()?.format(date);
});
const disabledBefore = computed(() => {
const todayTs = parseTimestamp(today()) as Timestamp;
return addToDate(todayTs, { day: -1 }).date;

View 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>

View 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>

View File

@@ -1,7 +1,7 @@
<template>
<q-page padding>
<q-page>
<q-list>
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-sm">
<q-input
bottom-slots
v-model="bookingForm.name"
@@ -70,7 +70,7 @@
<script setup lang="ts">
import { reactive, ref, computed, watch } from 'vue';
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 BoatSelection from 'src/components/scheduling/BoatSelection.vue';
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';

View File

@@ -4,56 +4,65 @@
<!-- <navigation-bar @today="onToday" @prev="onPrev" @next="onNext" /> -->
<div class="row justify-center">
<div
style="display: flex; max-width: 800px; width: 100%; height: 400px"
<q-calendar-day
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
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-intervals="onClickHeadIntervals"
@click-head-day="onClickHeadDay"
<template
#day-body="{
scope: { timestamp, timeStartPos, timeDurationHeight },
}"
>
<template #day-body="{ scope: { timestamp } }">
<template
v-for="event in scheduleStore.getBoatReservations(
makeDateTime(timestamp)
)"
:key="event.id"
<template
v-for="event in reservationEvents(timestamp)"
:key="event.id"
>
<div
v-if="event.start !== undefined"
class="booking-event"
:style="slotStyle(event, timeStartPos, timeDurationHeight)"
>
{{ timestamp }}
<div v-if="event.start !== undefined" class="my-event">
<div class="title q-calendar__ellipsis">
{{ event.user }}
<q-tooltip>{{
event.start + ' - ' + event.resource.name
}}</q-tooltip>
</div>
</div>
</template>
<span class="title q-calendar__ellipsis">
{{ event.user }}
<q-tooltip>{{
event.start + ' - ' + event.resource.name
}}</q-tooltip>
</span>
</div>
</template>
</q-calendar-day>
</div>
</template>
</q-calendar-day>
</div>
</div>
</q-page>
</template>
<script setup lang="ts">
import { useScheduleStore } from 'src/stores/schedule';
import { Reservation, useScheduleStore } from 'src/stores/schedule';
import { ref } from 'vue';
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());
@@ -61,6 +70,30 @@ const selectedDate = ref(today());
const calendarRef = ref(QCalendarDay);
// 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() {
calendarRef.value.moveToToday();
}
@@ -85,10 +118,28 @@ function onClickTime(data) {
function onClickInterval(data) {
console.log('onClickInterval', data);
}
function onClickHeadIntervals(data) {
console.log('onClickHeadIntervals', data);
}
function onClickHeadDay(data) {
console.log('onClickHeadDay', data);
}
</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>

View File

@@ -68,6 +68,14 @@ and rough engine performance.`,
year: 1989,
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', {

View File

@@ -3,8 +3,13 @@ import { ref } from 'vue';
import { Boat, useBoatStore } from './boat';
import { date } from 'quasar';
import { DateOptions } from 'quasar';
import { Timestamp } from '@quasar/quasar-ui-qcalendar';
import { timeStamp } from 'console';
import {
Timestamp,
parseDate,
updateRelative,
today,
parsed,
} from '@quasar/quasar-ui-qcalendar';
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
export type Reservation = {
@@ -16,32 +21,38 @@ export type Reservation = {
reservationDate: Date;
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.
e.g.: Should there be any qcalendar stuff in this store? Or should we have just JS Date
objects in here? */
export type Timeblock = {
id: number;
start: Timestamp;
end: Timestamp;
};
const sampleBlocks = [
{
start: { time: '09:00', hour: 9, minute: 0, hasDay: false, hasTime: true },
end: { time: '12:00', hour: 12, minute: 0, hasDay: false, hasTime: true },
id: 1,
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 },
end: { time: '15:00', hour: 15, minute: 0, hasDay: false, hasTime: true },
id: 2,
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 },
end: { time: '18:00', hour: 18, minute: 0, hasDay: false, hasTime: true },
id: 3,
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 },
end: { time: '21:00', hour: 21, minute: 0, hasDay: false, hasTime: true },
id: 4,
start: { time: '18:00', hour: 18, minute: 0, hasTime: true },
end: { time: '21:00', hour: 21, minute: 0, hasTime: true },
},
] as Timeblock[];
@@ -128,16 +139,26 @@ export const useScheduleStore = defineStore('schedule', () => {
const reservations = ref<Reservation[]>(getSampleReservations());
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 = (
searchDate: Date,
searchDate: Timestamp,
boat?: string
): Reservation[] => {
return reservations.value.filter((x) => {
console.log(searchDate);
return (
((x.start.getDate() == searchDate.getDate() ||
x.end.getDate() == searchDate.getDate()) && // Part of reservation falls on day
((parseDate(x.start)?.date == searchDate.date ||
parseDate(x.end)?.date == searchDate.date) && // Part of reservation falls on day
x.resource != undefined && // A boat is defined
!boat) ||
x.resource.$id == boat // A specific boat has been passed, and matches

View File

@@ -1120,18 +1120,23 @@
resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.11.tgz#84b1efb9097a6e58c3ebfdd5da83ac658056a35c"
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"
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==
dependencies:
"@quasar/quasar-ui-qcalendar" "^4.0.0-beta.16"
"@quasar/quasar-ui-qcalendar@^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==
"@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":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@quasar/render-ssr-error/-/render-ssr-error-1.0.3.tgz#33f27231007d1b222de41d3d70c29a6d14f9498a"