Sorted out a bunch of reactivity issues

This commit is contained in:
2024-05-29 10:00:48 -04:00
parent 6654132120
commit 387af2e6ce
10 changed files with 304 additions and 228 deletions

View File

@@ -5,6 +5,8 @@
<script setup lang="ts">
import { defineComponent, onMounted } from 'vue';
import { useAuthStore } from './stores/auth';
import { useBoatStore } from './stores/boat';
import { useReservationStore } from './stores/reservation';
defineComponent({
name: 'OYS Borrow-a-Boat',
@@ -12,5 +14,7 @@ defineComponent({
onMounted(async () => {
await useAuthStore().init();
await useBoatStore().fetchBoats();
await useReservationStore().fetchUserReservations();
});
</script>

View File

@@ -0,0 +1,246 @@
<template>
<div class="q-pa-xs row q-gutter-xs">
<q-card
flat
class="col-lg-4 col-md-6 col-sm-8 col-xs-12">
<q-card-section>
<div class="text-h5 q-mt-none q-mb-xs">
{{ reservation?.value ? 'Modify Booking' : 'New Booking' }}
</div>
<div class="text-caption text-grey-8">for: {{ bookingName }}</div>
</q-card-section>
<q-list class="q-px-xs">
<q-item
class="q-pa-none"
clickable
@click="boatSelect = true">
<q-card
v-if="boat"
class="col-12">
<q-card-section>
<q-img
:src="boat.imgSrc"
:fit="'scale-down'">
<div class="row absolute-top">
<div class="col text-h7 text-left">
{{ boat.name }}
</div>
<div class="col text-right text-caption">
{{ boat.class }}
</div>
</div>
</q-img>
</q-card-section>
<q-separator />
<q-card-section horizontal>
<q-card-section class="col-9">
<q-list
dense
class="row">
<q-item class="q-ma-none col-12">
<q-item-section avatar>
<q-badge
color="primary"
label="Start" />
</q-item-section>
<q-item-section class="text-body2">
{{ formatDate(bookingForm.interval?.start) }}
</q-item-section>
</q-item>
<q-item class="q-ma-none col-12">
<q-item-section avatar>
<q-badge
color="primary"
label="End" />
</q-item-section>
<q-item-section
class="text-body2"
style="min-width: 150px">
{{ formatDate(bookingForm.interval?.end) }}
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-separator vertical />
<q-card-section class="col-3 flex flex-center bg-grey-4">
{{ bookingDuration.hours }} hours
<div v-if="bookingDuration.minutes">
<q-separator />
{{ bookingDuration.minutes }} mins
</div>
</q-card-section>
</q-card-section>
</q-card>
<div
v-else
class="col-12">
<q-field filled>Tap to Select a Boat / Time</q-field>
</div>
</q-item>
<q-item class="q-px-none">
<q-item-section>
<q-select
filled
v-model="bookingForm.reason"
:options="reason_options"
label="Reason for sail" />
</q-item-section>
</q-item>
<q-item class="q-px-none">
<q-item-section>
<q-input
v-model="bookingForm.comment"
clearable
autogrow
filled
label="Additional Comments (optional)" />
</q-item-section>
</q-item>
</q-list>
<q-card-actions align="right">
<q-btn
label="Reset"
@click="onReset"
color="secondary"
size="md" />
<q-btn
label="Submit"
@click="onSubmit"
color="primary" />
</q-card-actions>
</q-card>
<q-dialog
v-model="boatSelect"
full-width>
<BoatScheduleTableComponent v-model="bookingForm.interval" />
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useAuthStore } from 'src/stores/auth';
import { Boat, useBoatStore } from 'src/stores/boat';
import { Interval, Reservation } from 'src/stores/schedule.types';
import BoatScheduleTableComponent from 'src/components/scheduling/boat/BoatScheduleTableComponent.vue';
import { formatDate } from 'src/utils/schedule';
import { useReservationStore } from 'src/stores/reservation';
import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router';
interface BookingForm {
$id?: string;
user?: string;
interval?: Interval | null;
reason?: string;
members?: string[];
guests?: string[];
comment?: string;
}
const reason_options = ['Open Sail', 'Private Sail', 'Racing', 'Other'];
const boatStore = useBoatStore();
const auth = useAuthStore();
const newForm = {
user: auth.currentUser?.$id,
interval: {} as Interval,
reason: 'Open Sail',
members: [],
guests: [],
comment: '',
};
const reservation = defineModel<Reservation>();
const reservationStore = useReservationStore();
const boatSelect = ref(false);
const bookingForm = ref<BookingForm>({ ...newForm });
const $q = useQuasar();
const router = useRouter();
watch(reservation, (newReservation) => {
if (!newReservation) {
bookingForm.value = newForm;
} else {
const updatedReservation = {
...newReservation,
interval: {
start: newReservation.start,
end: newReservation.end,
boatId: newReservation.resource,
},
};
bookingForm.value = updatedReservation;
}
});
const bookingDuration = computed((): { hours: number; minutes: number } => {
if (bookingForm.value.interval?.start && bookingForm.value.interval?.end) {
const start = new Date(bookingForm.value.interval.start).getTime();
const end = new Date(bookingForm.value.interval.end).getTime();
const delta = Math.abs(end - start) / 1000;
const hours = Math.floor(delta / 3600) % 24;
const minutes = Math.floor(delta - hours * 3600) % 60;
return { hours: hours, minutes: minutes };
}
return { hours: 0, minutes: 0 };
});
const bookingName = computed(() =>
auth.getUserNameById(bookingForm.value?.user)
);
const boat = computed((): Boat | null => {
const boatId = bookingForm.value.interval?.boatId;
console.log('Boat Lookup:', boatId);
return boatStore.getBoatById(boatId);
});
const onReset = () => {
bookingForm.value.interval = null;
bookingForm.value = reservation.value
? {
...reservation.value,
interval: {
start: reservation.value.start,
end: reservation.value.end,
boatId: reservation.value.resource,
},
}
: { ...newForm };
};
const onSubmit = () => {
const booking = bookingForm.value;
if (
!(
booking.interval &&
booking.interval.boatId &&
booking.interval.start &&
booking.interval.end &&
auth.currentUser
)
) {
// TODO: Make a proper validator
return;
}
const reservation = <Reservation>{
resource: booking.interval.boatId,
start: booking.interval.start,
end: booking.interval.end,
user: auth.currentUser.$id,
status: 'confirmed',
reason: booking.reason,
comment: booking.comment,
};
// TODO: Fix this. It will always look successful
reservationStore.createReservation(reservation); // Probably should pass the notify as a callback to the reservation creation.
$q.notify({
color: 'green-4',
textColor: 'white',
icon: 'cloud_done',
message: 'Submitted',
timeout: 3000,
});
router.go(-1);
};
</script>

View File

@@ -14,6 +14,8 @@
Start: {{ formatDate(reservation.start) }}
<br />
End: {{ formatDate(reservation.end) }}
<br />
Type: {{ reservation.reason }}
</p>
</div>
</div>
@@ -50,7 +52,7 @@
<q-card-actions v-if="!isPast(reservation.end)">
<q-btn
flat
@click="modifyReservation()">
:to="{ name: 'edit-reservation', params: { id: reservation.$id } }">
Modify
</q-btn>
<q-btn
@@ -107,8 +109,4 @@ const reservation = defineModel<Reservation>({ required: true });
const cancelReservation = () => {
cancelDialog.value = true;
};
const modifyReservation = () => {
return;
};
</script>

View File

@@ -1,217 +1,11 @@
<template>
<div class="q-pa-xs row q-gutter-xs">
<q-card
flat
class="col-lg-4 col-md-6 col-sm-8 col-xs-12">
<q-card-section>
<div class="text-h5 q-mt-none q-mb-xs">New Booking</div>
<div class="text-caption text-grey-8">for: {{ bookingForm.name }}</div>
</q-card-section>
<q-list class="q-px-xs">
<q-separator />
<q-item
class="q-px-none"
clickable
@click="boatSelect = true">
<q-item-section>
<q-card
v-if="bookingForm.boat"
flat>
<q-card-section>
<q-img
:src="bookingForm.boat?.imgSrc"
:fit="'scale-down'">
<div class="row absolute-top">
<div class="col text-h7 text-left">
{{ bookingForm.boat?.name }}
</div>
<div class="col text-right text-caption">
{{ bookingForm.boat?.class }}
</div>
</div>
</q-img>
</q-card-section>
<q-separator />
<q-card-section horizontal>
<q-card-section>
<q-list
dense
class="row">
<q-item>
<q-item-section avatar>
<q-badge
color="primary"
label="Start" />
</q-item-section>
<q-item-section class="text-body2">
{{ formatDate(bookingForm.startDate) }}
</q-item-section>
</q-item>
<q-item class="q-ma-none">
<q-item-section avatar>
<q-badge
color="primary"
label="End" />
</q-item-section>
<q-item-section class="text-body2">
{{ formatDate(bookingForm.endDate) }}
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-separator vertical />
<q-card-section class="col-3 flex flex-center bg-grey-4">
{{ bookingDuration.hours }} hours
<div v-if="bookingDuration.minutes">
<q-separator />
{{ bookingDuration.minutes }} mins
</div>
</q-card-section>
</q-card-section>
</q-card>
<q-field
readonly
filled
v-else>
Tap to Select a Boat / Time
</q-field>
</q-item-section>
</q-item>
<q-item class="q-px-none">
<q-item-section>
<q-select
filled
v-model="bookingForm.reason"
:options="reason_options"
label="Reason for sail" />
</q-item-section>
</q-item>
<q-item class="q-px-none">
<q-item-section>
<q-input
v-model="bookingForm.comment"
clearable
autogrow
filled
label="Additional Comments (optional)" />
</q-item-section>
</q-item>
</q-list>
<q-card-actions align="right">
<q-btn
label="Reset"
@click="onReset"
color="secondary"
size="md" />
<q-btn
label="Submit"
@click="onSubmit"
color="primary" />
</q-card-actions>
</q-card>
<q-dialog
v-model="boatSelect"
full-width>
<BoatScheduleTableComponent v-model="interval" />
</q-dialog>
</div>
<BoatReservationComponent v-model="newReservation" />
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useAuthStore } from 'src/stores/auth';
import { Boat, useBoatStore } from 'src/stores/boat';
import { useQuasar } from 'quasar';
import { Interval, Reservation } from 'src/stores/schedule.types';
import BoatScheduleTableComponent from 'src/components/scheduling/boat/BoatScheduleTableComponent.vue';
import { getNewId } from 'src/utils/misc';
import { formatDate } from 'src/utils/schedule';
import { useRouter } from 'vue-router';
import { useReservationStore } from 'src/stores/reservation';
import BoatReservationComponent from 'src/components/BoatReservationComponent.vue';
import { Reservation } from 'src/stores/schedule.types';
import { ref } from 'vue';
interface BookingForm {
bookingId: string;
name?: string;
boat?: Boat;
startDate?: string;
endDate?: string;
reason: string;
members: { name: string }[];
guests: { name: string }[];
comment?: string;
}
const reason_options = ['Open Sail', 'Private Sail', 'Racing', 'Other'];
const auth = useAuthStore();
const interval = ref<Interval>();
const newForm = {
bookingId: getNewId(),
name: auth.currentUser?.name,
boat: <Boat | undefined>undefined,
startDate: '',
endDate: '',
reason: 'Open Sail',
members: [],
guests: [],
comment: '',
};
const bookingForm = ref<BookingForm>({ ...newForm });
const router = useRouter();
const reservationStore = useReservationStore();
const $q = useQuasar();
const boatSelect = ref(false);
watch(interval, (new_interval) => {
bookingForm.value.boat = useBoatStore().boats.find(
(b) => b.$id === new_interval?.boatId
);
bookingForm.value.startDate = new_interval?.start;
bookingForm.value.endDate = new_interval?.end;
});
const bookingDuration = computed((): { hours: number; minutes: number } => {
if (bookingForm.value.startDate && bookingForm.value.endDate) {
const start = new Date(bookingForm.value.startDate).getTime();
const end = new Date(bookingForm.value.endDate).getTime();
const delta = Math.abs(end - start) / 1000;
const hours = Math.floor(delta / 3600) % 24;
const minutes = Math.floor(delta - hours * 3600) % 60;
return { hours: hours, minutes: minutes };
}
return { hours: 0, minutes: 0 };
});
const onReset = () => {
interval.value = undefined;
bookingForm.value = { ...newForm };
};
const onSubmit = () => {
const booking = bookingForm.value;
if (
!(booking.boat && booking.startDate && booking.endDate && auth.currentUser)
) {
// TODO: Make a proper validator
return;
}
const reservation = <Reservation>{
resource: booking.boat.$id,
start: booking.startDate,
end: booking.endDate,
user: auth.currentUser.$id,
status: 'confirmed',
reason: booking.reason,
comment: booking.comment,
};
console.log(reservation);
// TODO: Fix this. It will always look successful
reservationStore.createReservation(reservation); // Probably should pass the notify as a callback to the reservation creation.
$q.notify({
color: 'green-4',
textColor: 'white',
icon: 'cloud_done',
message: 'Submitted',
});
router.go(-1);
};
const newReservation = ref<Reservation>();
</script>

View File

@@ -59,13 +59,11 @@
</q-tab-panels>
</template>
<script setup lang="ts">
import { useBoatStore } from 'src/stores/boat';
import { useReservationStore } from 'src/stores/reservation';
import ReservationCardComponent from 'src/components/scheduling/ReservationCardComponent.vue';
import { onMounted, ref } from 'vue';
import { ref } from 'vue';
const reservationStore = useReservationStore();
const boatStore = useBoatStore();
const tab = ref('upcoming');
// const showMarker = (
@@ -82,9 +80,4 @@ const tab = ref('upcoming');
// isPast(currentItemDate) && (nextItemDate === null || !isPast(nextItemDate))
// );
// };
onMounted(() => {
boatStore.fetchBoats();
reservationStore.fetchUserReservations();
});
</script>

View File

@@ -0,0 +1,18 @@
<template>
<BoatReservationComponent v-model="reservation" />
</template>
<script setup lang="ts">
import BoatReservationComponent from 'src/components/BoatReservationComponent.vue';
import { useReservationStore } from 'src/stores/reservation';
import { Reservation } from 'src/stores/schedule.types';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
const reservation = ref<Reservation>();
onMounted(async () => {
const id = useRoute().params.id as string;
reservation.value = await useReservationStore().getReservationById(id);
});
</script>

View File

@@ -42,8 +42,15 @@ const routes: RouteRecordRaw[] = [
},
{
path: 'list',
component: () => import('src/pages/schedule/ListBookingsPage.vue'),
name: 'list-bookings',
component: () =>
import('src/pages/schedule/ListReservationsPage.vue'),
name: 'list-reservations',
},
{
path: 'edit/:id',
component: () =>
import('src/pages/schedule/ModifyBoatReservation.vue'),
name: 'edit-reservation',
},
{
path: 'manage',

View File

@@ -50,7 +50,8 @@ export const useAuthStore = defineStore('auth', () => {
currentUser.value = await account.get();
}
function getUserNameById(id: string) {
function getUserNameById(id: string | undefined | null): string {
if (!id) return 'No User';
try {
if (!userNames.value[id]) {
userNames.value[id] = '';

View File

@@ -47,6 +47,19 @@ export const useReservationStore = defineStore('reservation', () => {
setDateLoaded(startDate, endDate, 'error');
}
};
const getReservationById = async (id: string) => {
try {
const response = await databases.getDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
id
);
return response as Reservation;
} catch (error) {
console.error('Failed to fetch reservation: ', error);
}
};
const createReservation = async (reservation: Reservation) => {
try {
const response = await databases.createDocument(
@@ -75,7 +88,6 @@ export const useReservationStore = defineStore('reservation', () => {
return false;
}
console.log(id);
try {
await databases.deleteDocument(
AppwriteIds.databaseId,
@@ -203,6 +215,7 @@ export const useReservationStore = defineStore('reservation', () => {
return {
getReservationsByDate,
getReservationById,
createReservation,
deleteReservation,
fetchReservationsForDateRange,

View File

@@ -10,6 +10,8 @@ export type Reservation = Partial<Models.Document> & {
status?: StatusTypes;
reason: string;
comment: string;
members?: string[];
guests?: string[];
};
// 24 hrs in advance only 2 weekday, and 1 weekend slot