Developing Booking Form

This commit is contained in:
2023-11-26 09:21:04 -05:00
parent 8200bcde52
commit a3cdbbfbbd
13 changed files with 423 additions and 172 deletions

View File

@@ -79,8 +79,16 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
// https: true
open: true, // opens browser window automatically
// https: true,
// open: true, // opens browser window automatically
port: 4000,
strictport: true,
// For reverse-proxying via haproxy
hmr: {
clientPort: 443,
protocol: 'wss',
timeout: 0,
},
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -23,7 +23,7 @@ const account = new Account(client);
const databases = new Databases(client);
let appRouter: Router;
export default boot(async ({ app, router }) => {
export default boot(async ({ router }) => {
// Initialize store
const authStore = useAuthStore();
await authStore.init();

View File

@@ -0,0 +1,134 @@
<template>
<div class="row justify-center">
<q-btn-group rounded>
<q-btn
color="primary"
rounded
icon="keyboard_arrow_left"
label="Prev"
@click="onPrev"
/>
<q-btn
color="primary"
rounded
icon="today"
label="Today"
@click="onToday"
/>
<q-btn
color="primary"
rounded
icon-right="keyboard_arrow_right"
label="Next"
@click="onNext"
/>
</q-btn-group>
</div>
<div class="row justify-center">
<div style="display: flex; max-width: 1024px; width: 100%">
<q-calendar-resource
ref="calendar"
v-model="selectedDate"
:model-resources="boatStore.boats"
resource-key="id"
resource-label="name"
:interval-start="6"
:interval-count="12"
animated
bordered
@change="onChange"
@moved="onMoved"
@resource-expanded="onResourceExpanded"
@click-date="onClickDate"
@click-time="onClickTime"
@click-resource="onClickResource"
@click-head-resources="onClickHeadResources"
@click-interval="onClickInterval"
>
<template #resource-intervals="{ scope }">
<template v-for="(event, index) in getEvents(scope)" :key="index">
<q-badge
outline
color="primary"
:label="event.title"
:style="getStyle(event)"
/>
</template>
</template>
</q-calendar-resource>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { QCalendarResource, today } from '@quasar/quasar-ui-qcalendar';
import { Boat, useBoatStore } from 'src/stores/boat';
import { useBookingStore } from 'src/stores/booking';
const calendar = ref();
const boatStore = useBoatStore();
const bookingStore = useBookingStore();
type ResourceIntervalScope = {
resource: Boat;
intervals: [];
timeStartPosX(start: string): number;
timeDurationWidth(duration: number): number;
};
const selectedDate = ref(today());
function getEvents(scope: ResourceIntervalScope) {
const resourceEvents = bookingStore.bookings[scope.resource.id];
return resourceEvents.map((event) => ({
left: scope.timeStartPosX(event.start),
width: scope.timeDurationWidth(event.duration),
title: event.title,
}));
}
function getStyle(event: { left: number; width: number; title: string }) {
return {
position: 'absolute',
background: 'white',
left: `${event.left}px`,
width: `${event.width}px`,
height: '40px',
overflow: 'hidden',
};
}
function onToday() {
calendar.value.moveToToday();
}
function onPrev() {
calendar.value.prev();
}
function onNext() {
calendar.value.next();
}
function onMoved(data) {
console.log('onMoved', data);
}
function onChange(data) {
console.log('onChange', data);
}
function onResourceExpanded(data) {
console.log('onResourceExpanded', data);
}
function onClickDate(data) {
console.log('onClickDate', data);
}
function onClickTime(data) {
console.log('onClickTime', data);
}
function onClickResource(data) {
console.log('onClickResource', data);
}
function onClickHeadResources(data) {
console.log('onClickHeadResources', data);
}
function onClickInterval(data) {
console.log('onClickInterval', data);
}
</script>

View File

@@ -8,7 +8,6 @@
:key="link.name"
>
<q-btn
:v-model="auth.ready"
:icon="link.icon"
color="primary"
:size="`1.25em`"
@@ -25,8 +24,5 @@
<script lang="ts" setup>
import { links } from 'src/router/navlinks.js';
import { useAuthStore } from 'stores/auth';
import ToolbarComponent from 'components/ToolbarComponent.vue';
const auth = useAuthStore();
</script>

View File

@@ -1,165 +1,181 @@
<template>
<div class="row justify-center">
<q-btn-group rounded>
<q-btn
color="primary"
rounded
icon="keyboard_arrow_left"
label="Prev"
@click="onPrev"
/>
<q-btn
color="primary"
rounded
icon="today"
label="Today"
@click="onToday"
/>
<q-btn
color="primary"
rounded
icon-right="keyboard_arrow_right"
label="Next"
@click="onNext"
/>
</q-btn-group>
</div>
<div class="row justify-center">
<div style="display: flex; max-width: 1024px; width: 100%">
<q-calendar-resource
ref="calendar"
v-model="selectedDate"
:model-resources="resources"
resource-key="id"
resource-label="name"
:interval-start="6"
:interval-count="12"
animated
bordered
@change="onChange"
@moved="onMoved"
@resource-expanded="onResourceExpanded"
@click-date="onClickDate"
@click-time="onClickTime"
@click-resource="onClickResource"
@click-head-resources="onClickHeadResources"
@click-interval="onClickInterval"
>
<template #resource-intervals="{ scope }">
<template v-for="(event, index) in getEvents(scope)" :key="index">
<q-badge
outline
color="primary"
:label="event.title"
:style="getStyle(event)"
/>
<q-page padding>
<q-card>
<q-form>
<q-input bottom-slots v-model="bookingForm.name" label="Name" readonly>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</template>
</q-calendar-resource>
</div>
</div>
</q-input>
<q-select
v-model="bookingForm.boat"
:options="boats"
option-value="id"
option-label="name"
label="Boat"
>
<template v-slot:prepend>
<q-item-section avatar>
<q-img
v-if="bookingForm.boat?.iconsrc"
:src="bookingForm.boat?.iconsrc"
/>
<q-icon v-else name="sailing" />
</q-item-section>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-img :src="scope.opt.iconsrc" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
<q-item-label caption>{{ scope.opt.class }}</q-item-label>
</q-item-section>
<q-item-section avatar v-if="scope.opt.defects">
<q-icon name="warning" color="warning" />
<q-tooltip class="bg-amber text-black shadow-7"
>This boat has defects. Select it to see details.
</q-tooltip>
</q-item-section>
</q-item>
</template>
</q-select>
<q-banner
rounded
class="bg-warning text-grey-10"
v-if="bookingForm.boat?.defects"
>
<template v-slot:avatar>
<q-icon name="warning" color="grey-10" />
</template>
This boat currently has the following defects:
<ol>
<li
v-for="defect in bookingForm.boat.defects"
:key="defect.description"
>
{{ defect.description }}
</li>
</ol>
</q-banner>
<q-input v-model="bookingForm.startDate" label="Check-Out" readonly>
<template v-slot:prepend>
<q-icon name="event" class="cursor-pointer"> </q-icon>
</template>
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
@before-show="startDateOrTime = true"
>
<q-date
v-model="bookingForm.startDate"
mask="ddd MMM D, YYYY h:mm A"
:options="limitDate"
v-if="startDateOrTime"
>
<div class="row items-center justify-end">
<q-btn
label="Next"
color="primary"
flat
@click="startDateOrTime = false"
/>
</div>
</q-date>
<q-time
v-model="bookingForm.startDate"
mask="ddd MMM D, YYYY h:mm A"
:minute-options="minOpts"
v-else
>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-time>
</q-popup-proxy>
</q-input>
<q-input v-model="bookingForm.endDate" label="Check-In" readonly>
<template v-slot:prepend>
<q-icon name="event" class="cursor-pointer"> </q-icon>
</template>
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
@before-show="endDateOrTime = true"
>
<q-date
v-model="bookingForm.startDate"
mask="ddd MMM D, YYYY h:mm A"
:options="limitDate"
v-if="endDateOrTime"
>
<div class="row items-center justify-end">
<q-btn
label="Next"
color="primary"
flat
@click="endDateOrTime = !endDateOrTime"
/>
</div>
</q-date>
<q-time
v-model="bookingForm.endDate"
mask="ddd MMM D, YYYY h:mm A"
:minute-options="minOpts"
v-else
>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-time>
</q-popup-proxy>
</q-input>
<div>Booking Duration: {{ bookingDuration }} hours</div>
</q-form>
</q-card>
</q-page>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { QCalendarResource, today } from '@quasar/quasar-ui-qcalendar';
import { ref, computed } from 'vue';
import { useAuthStore } from 'src/stores/auth';
import { Boat, useBoatStore } from 'src/stores/boat';
import { date } from 'quasar';
const selectedDate = ref(today());
type Resource = {
id: string;
name: string;
expanded?: boolean;
children?: Resource[];
};
type Event = {
start: string;
title: string;
duration: number;
left?: number;
width?: number;
};
type ResourceIntervalScope = {
resource: Resource;
intervals: [];
timeStartPosX(start: string): number;
timeDurationWidth(duration: number): number;
};
const calendar = ref(null);
const resources = reactive<Resource[]>([
{ id: '1', name: 'ProjectX' },
{ id: '2', name: 'Take 5' },
{ id: '3', name: 'WeeBeestie' },
]);
const events = reactive<{ [key: string]: Event[] }>({
1: [
{ start: '06:00', title: 'John Smith', duration: 90 },
{ start: '12:00', title: 'Bob Barker', duration: 60 },
],
2: [
{ start: '08:00', title: 'Peter Parker', duration: 120 },
{ start: '11:00', title: 'Vince McMahon', duration: 240 },
],
3: [
{ start: '08:00', title: 'Heather Graham', duration: 240 },
{ start: '14:00', title: 'Lawrence Fishburne', duration: 60 },
],
const auth = useAuthStore();
const boats = useBoatStore().boats;
const startDateOrTime = ref(true);
const endDateOrTime = ref(true);
const bookingForm = ref({
name: auth.currentUser?.name,
boat: <Boat | undefined>undefined,
startDate: date.formatDate(new Date(), 'ddd MMM D, YYYY h:mm A'),
endDate: date.formatDate(
date.addToDate(new Date(), { hours: 2 }),
'ddd MMM D, YYYY h:mm A'
),
});
function getEvents(scope: ResourceIntervalScope) {
const resourceEvents = events[scope.resource.id];
return resourceEvents.map((event) => ({
left: scope.timeStartPosX(event.start),
width: scope.timeDurationWidth(event.duration),
title: event.title,
}));
}
const bookingDuration = computed(() => {
const diff = date.getDateDiff(
bookingForm.value.endDate,
bookingForm.value.startDate,
'hours'
);
return diff > 0 ? diff : 'Invalid.';
});
function getStyle(event: { left: number; width: number; title: string }) {
return {
position: 'absolute',
background: 'white',
left: `${event.left}px`,
width: `${event.width}px`,
height: '40px',
overflow: 'hidden',
};
}
const limitDate = (startDate: string) => {
return date.isBetweenDates(
startDate,
new Date(),
date.addToDate(new Date(), { days: 21 })
);
};
function onToday() {
calendar.value.moveToToday();
}
function onPrev() {
calendar.value.prev();
}
function onNext() {
calendar.value.next();
}
function onMoved(data) {
console.log('onMoved', data);
}
function onChange(data) {
console.log('onChange', data);
}
function onResourceExpanded(data) {
console.log('onResourceExpanded', data);
}
function onClickDate(data) {
console.log('onClickDate', data);
}
function onClickTime(data) {
console.log('onClickTime', data);
}
function onClickResource(data) {
console.log('onClickResource', data);
}
function onClickHeadResources(data) {
console.log('onClickHeadResources', data);
}
function onClickInterval(data) {
console.log('onClickInterval', data);
}
const minOpts = [0, 15, 30, 45, 60];
</script>

View File

@@ -0,0 +1,8 @@
<template>
<q-page padding>
<!-- content -->
</q-page>
</template>
<script setup lang="ts">
</script>

View File

@@ -1,5 +1,27 @@
<template>
<q-page padding> </q-page>
<q-page padding>
<q-item v-for="link in navlinks" :key="link.label">
<q-btn
:icon="link.icon"
color="primary"
size="1.25em"
:to="link.to"
:label="link.label"
rounded
class="full-width"
align="left"
/>
</q-item>
</q-page>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
const navlinks = [
{
icon: 'more_time',
to: '/schedule/book',
label: 'Create a Reservation',
},
{ icon: 'calendar_month', to: '/schedule/view', label: 'View Schedule' },
];
</script>

View File

@@ -11,8 +11,9 @@ import IndexPageVue from 'src/pages/IndexPage.vue';
import ProfilePageVue from 'src/pages/ProfilePage.vue';
import TaskPageVue from 'src/pages/TaskPage.vue';
import { RouteRecordRaw } from 'vue-router';
import BoatReservationPageVue from 'src/pages/schedule/BoatReservationPage.vue';
import SchedulePageView from 'pages/schedule/SchedulePageView.vue';
import BoatReservationPageVue from 'src/pages/schedule/BoatReservationPage.vue';
import BoatScheduleViewVue from 'src/pages/schedule/BoatScheduleView.vue';
const routes: RouteRecordRaw[] = [
{
@@ -48,6 +49,11 @@ const routes: RouteRecordRaw[] = [
component: BoatReservationPageVue,
name: 'reserve-boat',
},
{
path: 'view',
component: BoatScheduleViewVue,
name: 'boat-schedule',
},
],
},
{

View File

@@ -3,11 +3,12 @@ import { defineStore } from 'pinia';
// const boatSource = null;
export interface Boat {
id: number;
id: string;
name: string;
class: string;
year: number;
imgsrc: string;
class?: string;
year?: number;
imgsrc?: string;
iconsrc?: string;
booking?: {
available: boolean;
requiredCerts: string[];
@@ -18,27 +19,46 @@ export interface Boat {
type: string;
severity: string;
description: string;
detail: string;
detail?: string;
}[];
}
const getSampleData = () => [
{
id: 1,
id: '1',
name: 'ProjectX',
class: 'J/27',
year: 1981,
imgsrc: '/src/assets/j27.png',
iconsrc: '/src/assets/projectx_avatar256.png',
defects: [
{
type: 'engine',
severity: 'moderate',
description: 'Fuel line leaks at engine fitting.',
detail: `The gasket in the end of the fuel hose is damaged, and does not properly seal.
This will cause fuel to leak, and will allow air into the fuel chamber, causing a lean mixture,
and rough engine performance.`,
},
{
type: 'rigging',
severity: 'moderate',
description: 'Tiller extension is broken.',
detail:
'The tiller extension swivel is broken, and will not attach to the tiller.',
},
],
},
{
id: 2,
id: '2',
name: 'Take5',
class: 'J/27',
year: 1985,
imgsrc: '/src/assets/j27.png',
iconsrc: '/src/assets/take5_avatar32.png',
},
{
id: 3,
id: '3',
name: 'WeeBeestie',
class: 'Capri 25',
year: 1989,

41
src/stores/booking.ts Normal file
View File

@@ -0,0 +1,41 @@
import { defineStore } from 'pinia';
import { Boat } from './boat';
import { reactive } from 'vue';
export type Booking = {
start: string;
title: string;
duration: number;
left?: number;
width?: number;
};
export const useBookingStore = defineStore('bookings', () => {
const bookings = reactive<{ [key: string]: Booking[] }>({
1: [
{ start: '06:00', title: 'John Smith', duration: 90 },
{ start: '12:00', title: 'Bob Barker', duration: 60 },
],
2: [
{ start: '08:00', title: 'Peter Parker', duration: 120 },
{ start: '11:00', title: 'Vince McMahon', duration: 240 },
],
3: [
{ start: '08:00', title: 'Heather Graham', duration: 240 },
{ start: '14:00', title: 'Lawrence Fishburne', duration: 60 },
],
});
// So nested... Trying to do something perhaps too complicated?
// const bookingsForResource = (boatid: string) => (bookings) => {
// return bookings.filter((booking: Booking) => booking.key == boatid )
// }
// },
// actions: {
// increment () {
// this.counter++;
// }
// }
return { bookings };
});