Compare commits
4 Commits
59d2729719
...
b2420b270c
| Author | SHA1 | Date | |
|---|---|---|---|
|
b2420b270c
|
|||
|
9104ccab0f
|
|||
|
387af2e6ce
|
|||
|
6654132120
|
@@ -5,6 +5,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, onMounted } from 'vue';
|
import { defineComponent, onMounted } from 'vue';
|
||||||
import { useAuthStore } from './stores/auth';
|
import { useAuthStore } from './stores/auth';
|
||||||
|
import { useBoatStore } from './stores/boat';
|
||||||
|
import { useReservationStore } from './stores/reservation';
|
||||||
|
|
||||||
defineComponent({
|
defineComponent({
|
||||||
name: 'OYS Borrow-a-Boat',
|
name: 'OYS Borrow-a-Boat',
|
||||||
@@ -12,5 +14,7 @@ defineComponent({
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await useAuthStore().init();
|
await useAuthStore().init();
|
||||||
|
await useBoatStore().fetchBoats();
|
||||||
|
await useReservationStore().fetchUserReservations();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ const AppwriteIds = process.env.DEV
|
|||||||
? {
|
? {
|
||||||
databaseId: '65ee1cbf9c2493faf15f',
|
databaseId: '65ee1cbf9c2493faf15f',
|
||||||
collection: {
|
collection: {
|
||||||
boat: '66341910003e287cd71c',
|
boat: 'boat',
|
||||||
reservation: '663f8847000b8f5e29bb',
|
reservation: 'reservation',
|
||||||
skillTags: '66072582a74d94a4bd01',
|
skillTags: 'skillTags',
|
||||||
task: '65ee1cd5b550023fae4f',
|
task: 'task',
|
||||||
taskTags: '65ee21d72d5c8007c34c',
|
taskTags: 'taskTags',
|
||||||
interval: '66361869002883fb4c4b',
|
interval: 'interval',
|
||||||
intervalTemplate: '66361f480007fdd639af',
|
intervalTemplate: 'intervalTemplate',
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
userinfo: 'userinfo',
|
userinfo: 'userinfo',
|
||||||
|
|||||||
266
src/components/BoatReservationComponent.vue
Normal file
266
src/components/BoatReservationComponent.vue
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
<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,
|
||||||
|
resource: 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?.resource;
|
||||||
|
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,
|
||||||
|
resource: reservation.value.resource,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { ...newForm };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const booking = bookingForm.value;
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
booking.interval &&
|
||||||
|
booking.interval.resource &&
|
||||||
|
booking.interval.start &&
|
||||||
|
booking.interval.end &&
|
||||||
|
auth.currentUser
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// TODO: Make a proper validator
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const newReservation = <Reservation>{
|
||||||
|
resource: booking.interval.resource,
|
||||||
|
start: booking.interval.start,
|
||||||
|
end: booking.interval.end,
|
||||||
|
user: auth.currentUser.$id,
|
||||||
|
status: 'confirmed',
|
||||||
|
reason: booking.reason,
|
||||||
|
comment: booking.comment,
|
||||||
|
$id: reservation.value?.$id,
|
||||||
|
};
|
||||||
|
const status = $q.notify({
|
||||||
|
color: 'secondary',
|
||||||
|
textColor: 'white',
|
||||||
|
message: 'Submitting Reservation',
|
||||||
|
spinner: true,
|
||||||
|
closeBtn: 'Dismiss',
|
||||||
|
position: 'top',
|
||||||
|
timeout: 0,
|
||||||
|
group: false,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const r = await reservationStore.createOrUpdateReservation(newReservation);
|
||||||
|
status({
|
||||||
|
color: 'positive',
|
||||||
|
icon: 'cloud_done',
|
||||||
|
message: `Booking ${newReservation.$id ? 'updated' : 'created'}: ${
|
||||||
|
boatStore.getBoatById(r.resource)?.name
|
||||||
|
} at ${formatDate(r.start)}`,
|
||||||
|
spinner: false,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
status({
|
||||||
|
color: 'negative',
|
||||||
|
icon: 'error',
|
||||||
|
spinner: false,
|
||||||
|
message: 'Failed to book!' + e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
router.go(-1);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -21,7 +21,11 @@
|
|||||||
<q-icon :name="link.icon" />
|
<q-icon :name="link.icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section>{{ link.name }}</q-item-section>
|
<q-item-section>
|
||||||
|
<span :class="link.color ? `text-${link.color}` : ''">
|
||||||
|
{{ link.name }}
|
||||||
|
</span>
|
||||||
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-list v-if="link.sublinks">
|
<q-list v-if="link.sublinks">
|
||||||
<div
|
<div
|
||||||
@@ -36,7 +40,11 @@
|
|||||||
<q-icon :name="sublink.icon" />
|
<q-icon :name="sublink.icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section>{{ sublink.name }}</q-item-section>
|
<q-item-section>
|
||||||
|
<span :class="sublink.color ? `text-${sublink.color}` : ''">
|
||||||
|
{{ sublink.name }}
|
||||||
|
</span>
|
||||||
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</div>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
expand-icon-toggle
|
expand-icon-toggle
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, template)"
|
@dragstart="onDragStart($event, template)"
|
||||||
v-model="expanded"
|
v-model="expanded">
|
||||||
>
|
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -12,17 +11,21 @@
|
|||||||
:borderless="!edit"
|
:borderless="!edit"
|
||||||
dense
|
dense
|
||||||
v-model="template.name"
|
v-model="template.name"
|
||||||
v-if="edit"
|
v-if="edit" />
|
||||||
/><q-item-label v-if="!edit" class="cursor-pointer">{{
|
<q-item-label
|
||||||
template.name
|
v-if="!edit"
|
||||||
}}</q-item-label></q-item-section
|
class="cursor-pointer">
|
||||||
>
|
{{ template.name }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
</template>
|
</template>
|
||||||
<q-card flat>
|
<q-card flat>
|
||||||
<q-card-section horizontal>
|
<q-card-section horizontal>
|
||||||
<q-card-section class="q-pt-xs">
|
<q-card-section class="q-pt-xs">
|
||||||
<q-list dense>
|
<q-list dense>
|
||||||
<q-item v-for="(item, index) in template.timeTuples" :key="item[0]">
|
<q-item
|
||||||
|
v-for="(item, index) in template.timeTuples"
|
||||||
|
:key="item[0]">
|
||||||
<q-input
|
<q-input
|
||||||
class="q-mx-sm"
|
class="q-mx-sm"
|
||||||
dense
|
dense
|
||||||
@@ -38,8 +41,7 @@
|
|||||||
type="time"
|
type="time"
|
||||||
label="End"
|
label="End"
|
||||||
:borderless="!edit"
|
:borderless="!edit"
|
||||||
:readonly="!edit"
|
:readonly="!edit">
|
||||||
>
|
|
||||||
<template v-slot:after>
|
<template v-slot:after>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
@@ -47,46 +49,44 @@
|
|||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
icon="delete"
|
icon="delete"
|
||||||
@click="template.timeTuples.splice(index, 1)"
|
@click="template.timeTuples.splice(index, 1)" />
|
||||||
/> </template></q-input></q-item
|
</template>
|
||||||
></q-list>
|
</q-input>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
dense
|
dense
|
||||||
color="primary"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
label="Add interval"
|
label="Add interval"
|
||||||
@click="template.timeTuples.push(['00:00', '00:00'])"
|
@click="template.timeTuples.push(['00:00', '00:00'])" />
|
||||||
/></q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions vertical>
|
<q-card-actions vertical>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!edit"
|
v-if="!edit"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="edit"
|
icon="edit"
|
||||||
label="Edit"
|
label="Edit"
|
||||||
@click="toggleEdit"
|
@click="toggleEdit" />
|
||||||
/>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="save"
|
icon="save"
|
||||||
label="Save"
|
label="Save"
|
||||||
@click="saveTemplate($event, template)"
|
@click="saveTemplate($event, template)" />
|
||||||
/>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
icon="cancel"
|
icon="cancel"
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
@click="revert"
|
@click="revert" />
|
||||||
/>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
color="negative"
|
color="negative"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
label="Delete"
|
label="Delete"
|
||||||
v-if="template.$id !== ''"
|
v-if="template.$id !== ''"
|
||||||
@click="deleteTemplate($event, template)"
|
@click="deleteTemplate($event, template)" />
|
||||||
/>
|
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -101,25 +101,29 @@
|
|||||||
square
|
square
|
||||||
icon="schedule"
|
icon="schedule"
|
||||||
v-for="item in overlapped"
|
v-for="item in overlapped"
|
||||||
:key="item.start"
|
:key="item.start">
|
||||||
>
|
{{ item.start }}-{{ item.end }}
|
||||||
{{ item.start }}-{{ item.end }}</q-chip
|
</q-chip>
|
||||||
>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn flat label="OK" color="primary" v-close-popup />
|
<q-btn
|
||||||
</q-card-actions> </q-card
|
flat
|
||||||
></q-dialog>
|
label="OK"
|
||||||
|
color="primary"
|
||||||
|
v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useScheduleStore } from 'src/stores/schedule';
|
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
|
||||||
import { IntervalTemplate } from 'src/stores/schedule.types';
|
import { IntervalTemplate } from 'src/stores/schedule.types';
|
||||||
import { copyIntervalTemplate, timeTuplesOverlapped } from 'src/utils/schedule';
|
import { copyIntervalTemplate, timeTuplesOverlapped } from 'src/utils/schedule';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
const alert = ref(false);
|
const alert = ref(false);
|
||||||
const overlapped = ref();
|
const overlapped = ref();
|
||||||
const scheduleStore = useScheduleStore();
|
const intervalTemplateStore = useIntervalTemplateStore();
|
||||||
const props = defineProps<{ edit?: boolean; modelValue: IntervalTemplate }>();
|
const props = defineProps<{ edit?: boolean; modelValue: IntervalTemplate }>();
|
||||||
const edit = ref(props.edit);
|
const edit = ref(props.edit);
|
||||||
const expanded = ref(props.edit);
|
const expanded = ref(props.edit);
|
||||||
@@ -141,7 +145,7 @@ const deleteTemplate = (
|
|||||||
event: Event,
|
event: Event,
|
||||||
template: IntervalTemplate | undefined
|
template: IntervalTemplate | undefined
|
||||||
) => {
|
) => {
|
||||||
if (template?.$id) scheduleStore.deleteIntervalTemplate(template.$id);
|
if (template?.$id) intervalTemplateStore.deleteIntervalTemplate(template.$id);
|
||||||
};
|
};
|
||||||
|
|
||||||
function onDragStart(e: DragEvent, template: IntervalTemplate) {
|
function onDragStart(e: DragEvent, template: IntervalTemplate) {
|
||||||
@@ -159,9 +163,9 @@ const saveTemplate = (evt: Event, template: IntervalTemplate | undefined) => {
|
|||||||
} else {
|
} else {
|
||||||
edit.value = false;
|
edit.value = false;
|
||||||
if (template.$id && template.$id !== 'unsaved') {
|
if (template.$id && template.$id !== 'unsaved') {
|
||||||
scheduleStore.updateIntervalTemplate(template, template.$id);
|
intervalTemplateStore.updateIntervalTemplate(template, template.$id);
|
||||||
} else {
|
} else {
|
||||||
scheduleStore.createIntervalTemplate(template);
|
intervalTemplateStore.createIntervalTemplate(template);
|
||||||
emit('saved');
|
emit('saved');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
112
src/components/scheduling/ReservationCardComponent.vue
Normal file
112
src/components/scheduling/ReservationCardComponent.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<q-card
|
||||||
|
bordered
|
||||||
|
:class="isPast(reservation.end) ? 'text-blue-grey-6' : ''"
|
||||||
|
class="q-ma-md">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-h6">
|
||||||
|
{{ boatStore.getBoatById(reservation.resource)?.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle2">
|
||||||
|
<p>
|
||||||
|
Start: {{ formatDate(reservation.start) }}
|
||||||
|
<br />
|
||||||
|
End: {{ formatDate(reservation.end) }}
|
||||||
|
<br />
|
||||||
|
Type: {{ reservation.reason }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="col-auto">
|
||||||
|
<q-btn
|
||||||
|
color="grey-7"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
icon="more_vert">
|
||||||
|
<q-menu
|
||||||
|
cover
|
||||||
|
auto-close>
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>remove card</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>send feedback</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>share</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- <q-card-section>Some more information here...</q-card-section> -->
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-actions v-if="!isPast(reservation.end)">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:to="{ name: 'edit-reservation', params: { id: reservation.$id } }">
|
||||||
|
Modify
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
@click="cancelReservation()">
|
||||||
|
Cancel
|
||||||
|
</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
<q-dialog v-model="cancelDialog">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="row items-center">
|
||||||
|
<q-avatar
|
||||||
|
icon="warning"
|
||||||
|
color="negative"
|
||||||
|
text-color="white" />
|
||||||
|
<span class="q-ml-md">Warning!</span>
|
||||||
|
<p class="q-pt-md">
|
||||||
|
This will delete your reservation for
|
||||||
|
{{ boatStore.getBoatById(reservation?.resource)?.name }} on
|
||||||
|
{{ formatDate(reservation?.start) }}
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
label="Cancel"
|
||||||
|
color="primary"
|
||||||
|
v-close-popup />
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
label="Delete"
|
||||||
|
color="negative"
|
||||||
|
@click="reservationStore.deleteReservation(reservation)"
|
||||||
|
v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBoatStore } from 'src/stores/boat';
|
||||||
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
|
import type { Reservation } from 'src/stores/schedule.types';
|
||||||
|
import { formatDate, isPast } from 'src/utils/schedule';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const cancelDialog = ref(false);
|
||||||
|
const boatStore = useBoatStore();
|
||||||
|
const reservationStore = useReservationStore();
|
||||||
|
|
||||||
|
const reservation = defineModel<Reservation>({ required: true });
|
||||||
|
|
||||||
|
const cancelReservation = () => {
|
||||||
|
cancelDialog.value = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -35,7 +35,10 @@
|
|||||||
|
|
||||||
<template #day-body="{ scope }">
|
<template #day-body="{ scope }">
|
||||||
<div
|
<div
|
||||||
v-for="block in getBoatBlocks(scope)"
|
v-for="block in getAvailableIntervals(
|
||||||
|
scope.timestamp,
|
||||||
|
boats[scope.columnIndex]
|
||||||
|
)"
|
||||||
:key="block.$id">
|
:key="block.$id">
|
||||||
<div
|
<div
|
||||||
class="timeblock"
|
class="timeblock"
|
||||||
@@ -95,23 +98,24 @@ import CalendarHeaderComponent from './CalendarHeaderComponent.vue';
|
|||||||
|
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useBoatStore } from 'src/stores/boat';
|
import { useBoatStore } from 'src/stores/boat';
|
||||||
import { useScheduleStore } from 'src/stores/schedule';
|
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
import { Interval, Reservation } from 'src/stores/schedule.types';
|
import { Interval, Reservation } from 'src/stores/schedule.types';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useReservationStore } from 'src/stores/reservation';
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
|
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
|
||||||
|
import { useIntervalStore } from 'src/stores/interval';
|
||||||
|
|
||||||
const scheduleStore = useScheduleStore();
|
const intervalTemplateStore = useIntervalTemplateStore();
|
||||||
const reservationStore = useReservationStore();
|
const reservationStore = useReservationStore();
|
||||||
const { boats } = storeToRefs(useBoatStore());
|
const { boats } = storeToRefs(useBoatStore());
|
||||||
const selectedBlock = defineModel<Interval | null>();
|
const selectedBlock = defineModel<Interval | null>();
|
||||||
const selectedDate = ref(today());
|
const selectedDate = ref(today());
|
||||||
|
const { getAvailableIntervals } = useIntervalStore();
|
||||||
const calendar = ref<QCalendarDay | null>(null);
|
const calendar = ref<QCalendarDay | null>(null);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await useBoatStore().fetchBoats();
|
await useBoatStore().fetchBoats();
|
||||||
await scheduleStore.fetchIntervalTemplates();
|
await intervalTemplateStore.fetchIntervalTemplates();
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSwipe({ ...event }) {
|
function handleSwipe({ ...event }) {
|
||||||
@@ -194,23 +198,6 @@ function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Interval) {
|
|||||||
: (selectedBlock.value = block);
|
: (selectedBlock.value = block);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boatBlocks = computed((): Record<string, Interval[]> => {
|
|
||||||
return scheduleStore
|
|
||||||
.getIntervals(selectedDate.value)
|
|
||||||
.reduce((result, interval) => {
|
|
||||||
if (!result[interval.boatId]) result[interval.boatId] = [];
|
|
||||||
if (
|
|
||||||
!reservationStore.isResourceTimeOverlapped(
|
|
||||||
interval.boatId,
|
|
||||||
new Date(interval.start),
|
|
||||||
new Date(interval.end)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result[interval.boatId].push(interval);
|
|
||||||
return result;
|
|
||||||
}, <Record<string, Interval[]>>{});
|
|
||||||
});
|
|
||||||
|
|
||||||
const boatReservations = computed((): Record<string, Reservation[]> => {
|
const boatReservations = computed((): Record<string, Reservation[]> => {
|
||||||
return reservationStore
|
return reservationStore
|
||||||
.getReservationsByDate(selectedDate.value)
|
.getReservationsByDate(selectedDate.value)
|
||||||
@@ -220,11 +207,6 @@ const boatReservations = computed((): Record<string, Reservation[]> => {
|
|||||||
return result;
|
return result;
|
||||||
}, <Record<string, Reservation[]>>{});
|
}, <Record<string, Reservation[]>>{});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getBoatBlocks(scope: DayBodyScope): Interval[] {
|
|
||||||
const boat = boats.value[scope.columnIndex];
|
|
||||||
return boat ? boatBlocks.value[boat.$id] : [];
|
|
||||||
}
|
|
||||||
function getBoatReservations(scope: DayBodyScope): Reservation[] {
|
function getBoatReservations(scope: DayBodyScope): Reservation[] {
|
||||||
const boat = boats.value[scope.columnIndex];
|
const boat = boats.value[scope.columnIndex];
|
||||||
return boat ? boatReservations.value[boat.$id] : [];
|
return boat ? boatReservations.value[boat.$id] : [];
|
||||||
|
|||||||
@@ -1,217 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="q-pa-xs row q-gutter-xs">
|
<BoatReservationComponent v-model="newReservation" />
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import BoatReservationComponent from 'src/components/BoatReservationComponent.vue';
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { Reservation } from 'src/stores/schedule.types';
|
||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
import { ref } from 'vue';
|
||||||
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';
|
|
||||||
|
|
||||||
interface BookingForm {
|
const newReservation = ref<Reservation>();
|
||||||
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);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page>
|
||||||
<q-card class="subcontent">
|
<q-card class="row">
|
||||||
<navigation-bar
|
<navigation-bar
|
||||||
@today="onToday"
|
@today="onToday"
|
||||||
@prev="onPrev"
|
@prev="onPrev"
|
||||||
@@ -13,8 +13,10 @@
|
|||||||
resource-label="displayName"
|
resource-label="displayName"
|
||||||
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
|
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
|
||||||
:view="$q.screen.gt.md ? 'week' : 'day'"
|
:view="$q.screen.gt.md ? 'week' : 'day'"
|
||||||
bordered
|
v-touch-swipe.mouse.left.right="handleSwipe"
|
||||||
|
:max-days="$q.screen.lt.sm ? 3 : 7"
|
||||||
animated
|
animated
|
||||||
|
style="--calendar-resources-width: 2em"
|
||||||
day-min-height="50px"
|
day-min-height="50px"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
@moved="onMoved"
|
@moved="onMoved"
|
||||||
@@ -24,19 +26,26 @@
|
|||||||
@click-head-day="onClickHeadDay">
|
@click-head-day="onClickHeadDay">
|
||||||
<template #day="{ scope }">
|
<template #day="{ scope }">
|
||||||
<div
|
<div
|
||||||
v-for="event in boatReservationEvents(scope)"
|
v-for="interval in getSortedIntervals(
|
||||||
:key="event.id">
|
scope.timestamp,
|
||||||
<div
|
scope.resource
|
||||||
v-if="event.start !== undefined"
|
)"
|
||||||
class="booking-event">
|
:key="interval.$id"
|
||||||
<span class="title q-calendar__ellipsis">
|
class="row q-pb-xs">
|
||||||
{{ useAuthStore().getUserNameById(event.user) }} @
|
<q-badge
|
||||||
{{ renderTime(event.start) }}
|
multi-line
|
||||||
<q-tooltip>
|
class="col-12"
|
||||||
{{ renderTime(event.start) + ' - ' + renderTime(event.end) }}
|
:transparent="interval.user != undefined"
|
||||||
</q-tooltip>
|
:color="interval.user ? 'secondary' : 'primary'"
|
||||||
</span>
|
:id="interval.id">
|
||||||
</div>
|
{{
|
||||||
|
interval.user
|
||||||
|
? useAuthStore().getUserNameById(interval.user)
|
||||||
|
: 'Available'
|
||||||
|
}}
|
||||||
|
<br />
|
||||||
|
{{ formatTime(interval.start) }} - {{ formatTime(interval.end) }}
|
||||||
|
</q-badge>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-calendar-scheduler>
|
</q-calendar-scheduler>
|
||||||
@@ -46,7 +55,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useReservationStore } from 'src/stores/reservation';
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
import { onMounted, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
|
||||||
const reservationStore = useReservationStore();
|
const reservationStore = useReservationStore();
|
||||||
@@ -56,27 +65,31 @@ import { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
|||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
|
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
|
import { formatTime } from 'src/utils/schedule';
|
||||||
|
import { useIntervalStore } from 'src/stores/interval';
|
||||||
|
import { Interval } from 'src/stores/schedule.types';
|
||||||
|
|
||||||
const selectedDate = ref(today());
|
const selectedDate = ref(today());
|
||||||
const boatStore = useBoatStore();
|
const boatStore = useBoatStore();
|
||||||
const calendar = ref();
|
const calendar = ref();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
const { getAvailableIntervals } = useIntervalStore();
|
||||||
|
|
||||||
interface DayScope {
|
// interface DayScope {
|
||||||
timestamp: Timestamp;
|
// timestamp: Timestamp;
|
||||||
columnIndex: number;
|
// columnIndex: number;
|
||||||
resource: object;
|
// resource: object;
|
||||||
resourceIndex: number;
|
// resourceIndex: number;
|
||||||
indentLevel: number;
|
// indentLevel: number;
|
||||||
activeDate: boolean;
|
// activeDate: boolean;
|
||||||
droppable: boolean;
|
// droppable: boolean;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const renderTime = (dateString: string) => {
|
const getSortedIntervals = (timestamp: Timestamp, boat?: Boat): Interval[] => {
|
||||||
const date = new Date(dateString);
|
return getAvailableIntervals(timestamp, boat)
|
||||||
return date.toLocaleTimeString();
|
.concat(boatReservationEvents(timestamp, boat))
|
||||||
|
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
||||||
};
|
};
|
||||||
onMounted(() => boatStore.fetchBoats());
|
|
||||||
// Method declarations
|
// Method declarations
|
||||||
|
|
||||||
// function slotStyle(
|
// function slotStyle(
|
||||||
@@ -98,7 +111,14 @@ onMounted(() => boatStore.fetchBoats());
|
|||||||
// return s;
|
// return s;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function boatReservationEvents({ timestamp, resource }: DayScope) {
|
function handleSwipe({ ...event }) {
|
||||||
|
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
|
||||||
|
}
|
||||||
|
function boatReservationEvents(
|
||||||
|
timestamp: Timestamp,
|
||||||
|
resource: Boat | undefined
|
||||||
|
) {
|
||||||
|
if (!resource) return [];
|
||||||
return reservationStore.getReservationsByDate(
|
return reservationStore.getReservationsByDate(
|
||||||
getDate(timestamp),
|
getDate(timestamp),
|
||||||
(resource as Boat).$id
|
(resource as Boat).$id
|
||||||
@@ -132,25 +152,3 @@ function onNext() {
|
|||||||
calendar.value.next();
|
calendar.value.next();
|
||||||
}
|
}
|
||||||
</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>
|
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
<template>
|
|
||||||
<q-card
|
|
||||||
clas="q-ma-md"
|
|
||||||
bordered
|
|
||||||
v-if="!reservations">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">You don't have any bookings!</div>
|
|
||||||
<div class="text-h8">Why don't you go make one?</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
icon="event"
|
|
||||||
:size="`1.25em`"
|
|
||||||
label="Book Now"
|
|
||||||
rounded
|
|
||||||
class="full-width"
|
|
||||||
:align="'left'"
|
|
||||||
to="/schedule/book" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
<template
|
|
||||||
v-else
|
|
||||||
v-for="(reservation, index) in sortedBookings"
|
|
||||||
:key="reservation.$id">
|
|
||||||
<q-toolbar
|
|
||||||
class="bg-secondary glossy text-white"
|
|
||||||
v-if="showMarker(index, sortedBookings)">
|
|
||||||
Past
|
|
||||||
</q-toolbar>
|
|
||||||
<q-card
|
|
||||||
bordered
|
|
||||||
:class="isPast(reservation.end) ? 'text-blue-grey-6' : ''"
|
|
||||||
class="q-ma-md">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap">
|
|
||||||
<div class="col">
|
|
||||||
<div class="text-h6">
|
|
||||||
{{ boatStore.getBoatById(reservation.resource)?.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-subtitle2">
|
|
||||||
<p>
|
|
||||||
Start: {{ formatDate(reservation.start) }}
|
|
||||||
<br />
|
|
||||||
End: {{ formatDate(reservation.end) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="col-auto">
|
|
||||||
<q-btn
|
|
||||||
color="grey-7"
|
|
||||||
round
|
|
||||||
flat
|
|
||||||
icon="more_vert">
|
|
||||||
<q-menu
|
|
||||||
cover
|
|
||||||
auto-close>
|
|
||||||
<q-list>
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section>remove card</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section>send feedback</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section>share</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- <q-card-section>Some more information here...</q-card-section> -->
|
|
||||||
|
|
||||||
<q-separator />
|
|
||||||
|
|
||||||
<q-card-actions v-if="!isPast(reservation.end)">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
@click="modifyReservation(reservation)">
|
|
||||||
Modify
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
@click="cancelReservation(reservation)">
|
|
||||||
Cancel
|
|
||||||
</q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
<q-dialog v-model="cancelDialog">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="row items-center">
|
|
||||||
<q-avatar
|
|
||||||
icon="stop"
|
|
||||||
color="negative"
|
|
||||||
text-color="white" />
|
|
||||||
<span class="q-ml-sm">
|
|
||||||
This will delete your reservation for
|
|
||||||
{{ boatStore.getBoatById(currentReservation?.resource) }} on
|
|
||||||
{{ formatDate(currentReservation?.start) }}
|
|
||||||
</span>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
label="Cancel"
|
|
||||||
color="primary"
|
|
||||||
v-close-popup />
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
label="Delete"
|
|
||||||
color="negative"
|
|
||||||
@click="reservationStore.deleteReservation(Reservation)"
|
|
||||||
v-close-popup />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useBoatStore } from 'src/stores/boat';
|
|
||||||
import { useReservationStore } from 'src/stores/reservation';
|
|
||||||
import { Reservation } from 'src/stores/schedule.types';
|
|
||||||
import { formatDate } from 'src/utils/schedule';
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
const reservationStore = useReservationStore();
|
|
||||||
const reservations = reservationStore.getUserReservations();
|
|
||||||
const boatStore = useBoatStore();
|
|
||||||
const currentReservation = ref<Reservation>();
|
|
||||||
const cancelDialog = ref(false);
|
|
||||||
|
|
||||||
const sortedBookings = computed(() =>
|
|
||||||
reservations.value
|
|
||||||
?.slice()
|
|
||||||
.sort((a, b) => new Date(b.start).getTime() - new Date(a.start).getTime())
|
|
||||||
);
|
|
||||||
|
|
||||||
const isPast = (itemDate: Date | string): boolean => {
|
|
||||||
if (!(itemDate instanceof Date)) {
|
|
||||||
itemDate = new Date(itemDate);
|
|
||||||
}
|
|
||||||
console.log(itemDate);
|
|
||||||
const currentDate = new Date();
|
|
||||||
return itemDate < currentDate;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showMarker = (
|
|
||||||
index: number,
|
|
||||||
items: Reservation[] | undefined
|
|
||||||
): boolean => {
|
|
||||||
if (!items) return false;
|
|
||||||
|
|
||||||
const currentItemDate = new Date(items[index].start);
|
|
||||||
const nextItemDate = index > 0 ? new Date(items[index - 1].start) : null;
|
|
||||||
|
|
||||||
// Show marker if current item is past and the next item is future or vice versa
|
|
||||||
return (
|
|
||||||
isPast(currentItemDate) && (nextItemDate === null || !isPast(nextItemDate))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelReservation = (reservation: Reservation) => {
|
|
||||||
currentReservation.value = reservation;
|
|
||||||
cancelDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const modifyReservation = (reservation: Reservation) => {
|
|
||||||
return reservation;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
boatStore.fetchBoats();
|
|
||||||
reservationStore.fetchUserReservations();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
86
src/pages/schedule/ListReservationsPage.vue
Normal file
86
src/pages/schedule/ListReservationsPage.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab"
|
||||||
|
inline-label
|
||||||
|
class="text-primary">
|
||||||
|
<q-tab
|
||||||
|
name="upcoming"
|
||||||
|
icon="schedule"
|
||||||
|
label="Upcoming" />
|
||||||
|
<q-tab
|
||||||
|
name="past"
|
||||||
|
icon="history"
|
||||||
|
label="Past" />
|
||||||
|
</q-tabs>
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-tab-panels
|
||||||
|
v-model="tab"
|
||||||
|
animated>
|
||||||
|
<q-tab-panel
|
||||||
|
name="upcoming"
|
||||||
|
class="q-pa-none">
|
||||||
|
<q-card
|
||||||
|
clas="q-ma-md"
|
||||||
|
v-if="!futureUserReservations.length">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">You don't have any upcoming bookings!</div>
|
||||||
|
<div class="text-h8">Why don't you go make one?</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="event"
|
||||||
|
:size="`1.25em`"
|
||||||
|
label="Book Now"
|
||||||
|
rounded
|
||||||
|
class="full-width"
|
||||||
|
:align="'left'"
|
||||||
|
to="/schedule/book" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="reservation in futureUserReservations"
|
||||||
|
:key="reservation.$id">
|
||||||
|
<ReservationCardComponent :modelValue="reservation" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel
|
||||||
|
name="past"
|
||||||
|
class="q-pa-none">
|
||||||
|
<div
|
||||||
|
v-for="reservation in pastUserReservations"
|
||||||
|
:key="reservation.$id">
|
||||||
|
<ReservationCardComponent :modelValue="reservation" />
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
|
import ReservationCardComponent from 'src/components/scheduling/ReservationCardComponent.vue';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const { futureUserReservations, pastUserReservations } = useReservationStore();
|
||||||
|
|
||||||
|
onMounted(() => useReservationStore().fetchUserReservations());
|
||||||
|
|
||||||
|
const tab = ref('upcoming');
|
||||||
|
|
||||||
|
// const showMarker = (
|
||||||
|
// index: number,
|
||||||
|
// items: Reservation[] | undefined
|
||||||
|
// ): boolean => {
|
||||||
|
// if (!items) return false;
|
||||||
|
|
||||||
|
// const currentItemDate = new Date(items[index].start);
|
||||||
|
// const nextItemDate = index > 0 ? new Date(items[index - 1].start) : null;
|
||||||
|
|
||||||
|
// // Show marker if current item is past and the next item is future or vice versa
|
||||||
|
// return (
|
||||||
|
// isPast(currentItemDate) && (nextItemDate === null || !isPast(nextItemDate))
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
</script>
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fit row wrap justify-start items-start content-start">
|
<div class="fit row wrap justify-start items-start content-start">
|
||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<div class="scheduler" style="max-width: 1200px">
|
<div
|
||||||
<NavigationBar @next="onNext" @today="onToday" @prev="onPrev" />
|
class="scheduler"
|
||||||
|
style="max-width: 1200px">
|
||||||
|
<NavigationBar
|
||||||
|
@next="onNext"
|
||||||
|
@today="onToday"
|
||||||
|
@prev="onPrev" />
|
||||||
<q-calendar-scheduler
|
<q-calendar-scheduler
|
||||||
ref="calendar"
|
ref="calendar"
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
@@ -18,8 +23,7 @@
|
|||||||
:drag-leave-func="onDragLeave"
|
:drag-leave-func="onDragLeave"
|
||||||
:drop-func="onDrop"
|
:drop-func="onDrop"
|
||||||
day-min-height="50px"
|
day-min-height="50px"
|
||||||
cell-width="150px"
|
cell-width="150px">
|
||||||
>
|
|
||||||
<template #day="{ scope }">
|
<template #day="{ scope }">
|
||||||
<div
|
<div
|
||||||
v-if="filteredIntervals(scope.timestamp, scope.resource).length"
|
v-if="filteredIntervals(scope.timestamp, scope.resource).length"
|
||||||
@@ -29,15 +33,13 @@
|
|||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<template
|
<template
|
||||||
v-for="block in sortedIntervals(
|
v-for="block in sortedIntervals(
|
||||||
scope.timestamp,
|
scope.timestamp,
|
||||||
scope.resource
|
scope.resource
|
||||||
)"
|
)"
|
||||||
:key="block.id"
|
:key="block.id">
|
||||||
>
|
|
||||||
<q-chip class="cursor-pointer">
|
<q-chip class="cursor-pointer">
|
||||||
{{ date.formatDate(block.start, 'HH:mm') }} -
|
{{ date.formatDate(block.start, 'HH:mm') }} -
|
||||||
{{ date.formatDate(block.end, 'HH:mm') }}
|
{{ date.formatDate(block.end, 'HH:mm') }}
|
||||||
@@ -77,46 +79,53 @@
|
|||||||
).toISOString())
|
).toISOString())
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</q-popup-edit>--> </q-chip
|
</q-popup-edit>-->
|
||||||
><q-btn
|
</q-chip>
|
||||||
|
<q-btn
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
round
|
round
|
||||||
@click="deleteBlock(block)"
|
@click="deleteBlock(block)" />
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-calendar-scheduler>
|
</q-calendar-scheduler>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="q-pa-md" style="width: 400px">
|
<div
|
||||||
<q-list padding bordered class="rounded-borders">
|
class="q-pa-md"
|
||||||
|
style="width: 400px">
|
||||||
|
<q-list
|
||||||
|
padding
|
||||||
|
bordered
|
||||||
|
class="rounded-borders">
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label overline>Availability Templates</q-item-label>
|
<q-item-label overline>Availability Templates</q-item-label>
|
||||||
<q-item-label caption
|
<q-item-label caption>
|
||||||
>Drag and drop a template to a boat / date to create booking
|
Drag and drop a template to a boat / date to create booking
|
||||||
availability</q-item-label
|
availability
|
||||||
>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn label="Add Template" color="primary" @click="createTemplate" />
|
<q-btn
|
||||||
|
label="Add Template"
|
||||||
|
color="primary"
|
||||||
|
@click="createTemplate" />
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
<q-item v-if="newTemplate.$id === 'unsaved'"
|
<q-item v-if="newTemplate.$id === 'unsaved'">
|
||||||
><IntervalTemplateComponent
|
<IntervalTemplateComponent
|
||||||
:model-value="newTemplate"
|
:model-value="newTemplate"
|
||||||
:edit="true"
|
:edit="true"
|
||||||
@cancel="resetNewTemplate"
|
@cancel="resetNewTemplate"
|
||||||
@saved="resetNewTemplate"
|
@saved="resetNewTemplate" />
|
||||||
/></q-item>
|
</q-item>
|
||||||
<q-separator spaced />
|
<q-separator spaced />
|
||||||
<IntervalTemplateComponent
|
<IntervalTemplateComponent
|
||||||
v-for="template in intervalTemplates"
|
v-for="template in intervalTemplates"
|
||||||
:key="template.$id"
|
:key="template.$id"
|
||||||
:model-value="template"
|
:model-value="template" />
|
||||||
/>
|
|
||||||
</q-list>
|
</q-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,16 +136,23 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pt-none">
|
<q-card-section class="q-pt-none">
|
||||||
Conflicting times! Please delete overlapped items!
|
Conflicting times! Please delete overlapped items!
|
||||||
<q-chip v-for="item in overlapped" :key="item.index"
|
<q-chip
|
||||||
>{{ boats.find((b) => b.$id === item.boatId)?.name }}:
|
v-for="item in overlapped"
|
||||||
|
:key="item.index">
|
||||||
|
{{ boats.find((b) => b.$id === item.boatId)?.name }}:
|
||||||
{{ date.formatDate(item.start, 'hh:mm') }} -
|
{{ date.formatDate(item.start, 'hh:mm') }} -
|
||||||
{{ date.formatDate(item.end, 'hh:mm') }}
|
{{ date.formatDate(item.end, 'hh:mm') }}
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn flat label="OK" color="primary" v-close-popup />
|
<q-btn
|
||||||
</q-card-actions> </q-card
|
flat
|
||||||
></q-dialog>
|
label="OK"
|
||||||
|
color="primary"
|
||||||
|
v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -146,7 +162,7 @@ import {
|
|||||||
today,
|
today,
|
||||||
} from '@quasar/quasar-ui-qcalendar';
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
import { useScheduleStore } from 'src/stores/schedule';
|
import { useIntervalStore } from 'src/stores/interval';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
Interval,
|
Interval,
|
||||||
@@ -158,12 +174,14 @@ import IntervalTemplateComponent from 'src/components/scheduling/IntervalTemplat
|
|||||||
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
|
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { buildInterval, intervalsOverlapped } from 'src/utils/schedule';
|
import { buildInterval, intervalsOverlapped } from 'src/utils/schedule';
|
||||||
|
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
|
||||||
|
|
||||||
const selectedDate = ref(today());
|
const selectedDate = ref(today());
|
||||||
const { fetchBoats } = useBoatStore();
|
const { fetchBoats } = useBoatStore();
|
||||||
const scheduleStore = useScheduleStore();
|
const intervalStore = useIntervalStore();
|
||||||
|
const intervalTemplateStore = useIntervalTemplateStore();
|
||||||
const { boats } = storeToRefs(useBoatStore());
|
const { boats } = storeToRefs(useBoatStore());
|
||||||
const intervalTemplates = scheduleStore.getIntervalTemplates();
|
const intervalTemplates = intervalTemplateStore.getIntervalTemplates();
|
||||||
const calendar = ref();
|
const calendar = ref();
|
||||||
const overlapped = ref();
|
const overlapped = ref();
|
||||||
const alert = ref(false);
|
const alert = ref(false);
|
||||||
@@ -182,11 +200,11 @@ const newTemplate = ref<IntervalTemplate>({
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchBoats();
|
await fetchBoats();
|
||||||
await scheduleStore.fetchIntervalTemplates();
|
await intervalTemplateStore.fetchIntervalTemplates();
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredIntervals = (date: Timestamp, boat: Boat) => {
|
const filteredIntervals = (date: Timestamp, boat: Boat) => {
|
||||||
return scheduleStore.getIntervals(date, boat);
|
return intervalStore.getIntervals(date, boat);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortedIntervals = (date: Timestamp, boat: Boat) => {
|
const sortedIntervals = (date: Timestamp, boat: Boat) => {
|
||||||
@@ -207,11 +225,11 @@ function createTemplate() {
|
|||||||
}
|
}
|
||||||
function createIntervals(boat: Boat, templateId: string, date: string) {
|
function createIntervals(boat: Boat, templateId: string, date: string) {
|
||||||
const intervals = intervalsFromTemplate(boat, templateId, date);
|
const intervals = intervalsFromTemplate(boat, templateId, date);
|
||||||
intervals.forEach((interval) => scheduleStore.createInterval(interval));
|
intervals.forEach((interval) => intervalStore.createInterval(interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIntervals(date: Timestamp, boat: Boat) {
|
function getIntervals(date: Timestamp, boat: Boat) {
|
||||||
return scheduleStore.getIntervals(date, boat);
|
return intervalStore.getIntervals(date, boat);
|
||||||
}
|
}
|
||||||
|
|
||||||
function intervalsFromTemplate(
|
function intervalsFromTemplate(
|
||||||
@@ -219,7 +237,7 @@ function intervalsFromTemplate(
|
|||||||
templateId: string,
|
templateId: string,
|
||||||
date: string
|
date: string
|
||||||
): Interval[] {
|
): Interval[] {
|
||||||
const template = scheduleStore
|
const template = intervalTemplateStore
|
||||||
.getIntervalTemplates()
|
.getIntervalTemplates()
|
||||||
.value.find((t) => t.$id === templateId);
|
.value.find((t) => t.$id === templateId);
|
||||||
return template
|
return template
|
||||||
@@ -231,7 +249,7 @@ function intervalsFromTemplate(
|
|||||||
|
|
||||||
function deleteBlock(block: Interval) {
|
function deleteBlock(block: Interval) {
|
||||||
if (block.$id) {
|
if (block.$id) {
|
||||||
scheduleStore.deleteInterval(block.$id);
|
intervalStore.deleteInterval(block.$id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
src/pages/schedule/ModifyBoatReservation.vue
Normal file
18
src/pages/schedule/ModifyBoatReservation.vue
Normal 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>
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<q-item v-for="link in navlinks" :key="link.name">
|
<q-item
|
||||||
|
v-for="link in navlinks"
|
||||||
|
:key="link.name">
|
||||||
<q-btn
|
<q-btn
|
||||||
:icon="link.icon"
|
:icon="link.icon"
|
||||||
color="primary"
|
:color="link.color ? link.color : 'primary'"
|
||||||
size="1.25em"
|
size="1.25em"
|
||||||
:to="link.to"
|
:to="link.to"
|
||||||
:label="link.name"
|
:label="link.name"
|
||||||
rounded
|
rounded
|
||||||
class="full-width"
|
class="full-width"
|
||||||
align="left"
|
align="left" />
|
||||||
/>
|
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
|
||||||
export const links = [
|
export type Link = {
|
||||||
|
name: string;
|
||||||
|
to: string;
|
||||||
|
icon: string;
|
||||||
|
front_links?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
color?: string;
|
||||||
|
sublinks?: Link[];
|
||||||
|
requiredRoles?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const links = <Link[]>[
|
||||||
{
|
{
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
to: '/',
|
to: '/',
|
||||||
@@ -30,7 +41,7 @@ export const links = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sublinks: [
|
sublinks: [
|
||||||
{
|
{
|
||||||
name: 'List',
|
name: 'My View',
|
||||||
to: '/schedule/list',
|
to: '/schedule/list',
|
||||||
icon: 'list',
|
icon: 'list',
|
||||||
front_links: false,
|
front_links: false,
|
||||||
@@ -44,7 +55,7 @@ export const links = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'View',
|
name: 'Calendar',
|
||||||
to: '/schedule/view',
|
to: '/schedule/view',
|
||||||
icon: 'calendar_month',
|
icon: 'calendar_month',
|
||||||
front_links: false,
|
front_links: false,
|
||||||
@@ -56,6 +67,7 @@ export const links = [
|
|||||||
icon: 'edit_calendar',
|
icon: 'edit_calendar',
|
||||||
front_links: false,
|
front_links: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
color: 'accent',
|
||||||
requiredRoles: ['Schedule Admins'],
|
requiredRoles: ['Schedule Admins'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -42,8 +42,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'list',
|
path: 'list',
|
||||||
component: () => import('src/pages/schedule/ListBookingsPage.vue'),
|
component: () =>
|
||||||
name: 'list-bookings',
|
import('src/pages/schedule/ListReservationsPage.vue'),
|
||||||
|
name: 'list-reservations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
component: () =>
|
||||||
|
import('src/pages/schedule/ModifyBoatReservation.vue'),
|
||||||
|
name: 'edit-reservation',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'manage',
|
path: 'manage',
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
currentUser.value = await account.get();
|
currentUser.value = await account.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserNameById(id: string) {
|
function getUserNameById(id: string | undefined | null): string {
|
||||||
|
if (!id) return 'No User';
|
||||||
try {
|
try {
|
||||||
if (!userNames.value[id]) {
|
if (!userNames.value[id]) {
|
||||||
userNames.value[id] = '';
|
userNames.value[id] = '';
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { Ref, computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { Boat } from './boat';
|
import { Boat } from './boat';
|
||||||
import { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
import { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
||||||
|
|
||||||
import { IntervalTemplate, Interval, IntervalRecord } from './schedule.types';
|
import { Interval, IntervalRecord } from './schedule.types';
|
||||||
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
||||||
import { ID, Models, Query } from 'appwrite';
|
import { ID, Query } from 'appwrite';
|
||||||
import { arrayToTimeTuples } from 'src/utils/schedule';
|
import { useReservationStore } from './reservation';
|
||||||
|
|
||||||
export const useScheduleStore = defineStore('schedule', () => {
|
export const useIntervalStore = defineStore('interval', () => {
|
||||||
// TODO: Implement functions to dynamically pull this data.
|
// TODO: Implement functions to dynamically pull this data.
|
||||||
const intervals = ref<Map<string, Interval>>(new Map());
|
const intervals = ref<Map<string, Interval>>(new Map());
|
||||||
const intervalDates = ref<IntervalRecord>({});
|
const intervalDates = ref<IntervalRecord>({});
|
||||||
const intervalTemplates = ref<IntervalTemplate[]>([]);
|
const reservationStore = useReservationStore();
|
||||||
|
|
||||||
const getIntervals = (date: Timestamp | string, boat?: Boat): Interval[] => {
|
const getIntervals = (date: Timestamp | string, boat?: Boat): Interval[] => {
|
||||||
const searchDate = typeof date === 'string' ? date : date.date;
|
const searchDate = typeof date === 'string' ? date : date.date;
|
||||||
@@ -28,12 +28,29 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
const intervalEnd = new Date(interval.end);
|
const intervalEnd = new Date(interval.end);
|
||||||
|
|
||||||
const isWithinDay = intervalStart < dayEnd && intervalEnd > dayStart;
|
const isWithinDay = intervalStart < dayEnd && intervalEnd > dayStart;
|
||||||
const matchesBoat = boat ? boat.$id === interval.boatId : true;
|
const matchesBoat = boat ? boat.$id === interval.resource : true;
|
||||||
return isWithinDay && matchesBoat;
|
return isWithinDay && matchesBoat;
|
||||||
});
|
});
|
||||||
}).value;
|
}).value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAvailableIntervals = (
|
||||||
|
date: Timestamp | string,
|
||||||
|
boat?: Boat
|
||||||
|
): Interval[] => {
|
||||||
|
return computed(() => {
|
||||||
|
console.log(boat);
|
||||||
|
console.log(getIntervals(date, boat));
|
||||||
|
return getIntervals(date, boat).filter((interval) => {
|
||||||
|
return !reservationStore.isResourceTimeOverlapped(
|
||||||
|
interval.resource,
|
||||||
|
new Date(interval.start),
|
||||||
|
new Date(interval.end)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}).value;
|
||||||
|
};
|
||||||
|
|
||||||
async function fetchIntervals(dateString: string) {
|
async function fetchIntervals(dateString: string) {
|
||||||
try {
|
try {
|
||||||
const response = await databases.listDocuments(
|
const response = await databases.listDocuments(
|
||||||
@@ -62,31 +79,6 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIntervalTemplates = (): Ref<IntervalTemplate[]> => {
|
|
||||||
// Should subscribe to get new intervaltemplates when they are created
|
|
||||||
if (!intervalTemplates.value) fetchIntervalTemplates();
|
|
||||||
return intervalTemplates;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function fetchIntervalTemplates() {
|
|
||||||
try {
|
|
||||||
const response = await databases.listDocuments(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.intervalTemplate
|
|
||||||
);
|
|
||||||
intervalTemplates.value = response.documents.map(
|
|
||||||
(d: Models.Document): IntervalTemplate => {
|
|
||||||
return {
|
|
||||||
...d,
|
|
||||||
timeTuples: arrayToTimeTuples(d.timeTuple),
|
|
||||||
} as IntervalTemplate;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch timeblock templates', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createInterval = async (interval: Interval) => {
|
const createInterval = async (interval: Interval) => {
|
||||||
try {
|
try {
|
||||||
const response = await databases.createDocument(
|
const response = await databases.createDocument(
|
||||||
@@ -131,70 +123,13 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
console.error('Error deleting Interval: ' + e);
|
console.error('Error deleting Interval: ' + e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const createIntervalTemplate = async (template: IntervalTemplate) => {
|
|
||||||
try {
|
|
||||||
const response = await databases.createDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.intervalTemplate,
|
|
||||||
ID.unique(),
|
|
||||||
{ name: template.name, timeTuple: template.timeTuples.flat(2) }
|
|
||||||
);
|
|
||||||
intervalTemplates.value.push(response as IntervalTemplate);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error updating IntervalTemplate: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const deleteIntervalTemplate = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await databases.deleteDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.intervalTemplate,
|
|
||||||
id
|
|
||||||
);
|
|
||||||
intervalTemplates.value = intervalTemplates.value.filter(
|
|
||||||
(template) => template.$id !== id
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error deleting IntervalTemplate: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const updateIntervalTemplate = async (
|
|
||||||
template: IntervalTemplate,
|
|
||||||
id: string
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const response = await databases.updateDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.intervalTemplate,
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
name: template.name,
|
|
||||||
timeTuple: template.timeTuples.flat(2),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
intervalTemplates.value = intervalTemplates.value.map((b) =>
|
|
||||||
b.$id !== id
|
|
||||||
? b
|
|
||||||
: ({
|
|
||||||
...response,
|
|
||||||
timeTuples: arrayToTimeTuples(response.timeTuple),
|
|
||||||
} as IntervalTemplate)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error updating IntervalTemplate: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getIntervals,
|
getIntervals,
|
||||||
getIntervalTemplates,
|
getAvailableIntervals,
|
||||||
fetchIntervals,
|
fetchIntervals,
|
||||||
fetchIntervalTemplates,
|
|
||||||
createInterval,
|
createInterval,
|
||||||
updateInterval,
|
updateInterval,
|
||||||
deleteInterval,
|
deleteInterval,
|
||||||
createIntervalTemplate,
|
|
||||||
deleteIntervalTemplate,
|
|
||||||
updateIntervalTemplate,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
97
src/stores/intervalTemplate.ts
Normal file
97
src/stores/intervalTemplate.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { Ref, ref } from 'vue';
|
||||||
|
import { IntervalTemplate } from './schedule.types';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
||||||
|
import { ID, Models } from 'appwrite';
|
||||||
|
import { arrayToTimeTuples } from 'src/utils/schedule';
|
||||||
|
|
||||||
|
export const useIntervalTemplateStore = defineStore('intervalTemplate', () => {
|
||||||
|
const intervalTemplates = ref<IntervalTemplate[]>([]);
|
||||||
|
|
||||||
|
const getIntervalTemplates = (): Ref<IntervalTemplate[]> => {
|
||||||
|
// Should subscribe to get new intervaltemplates when they are created
|
||||||
|
if (!intervalTemplates.value) fetchIntervalTemplates();
|
||||||
|
return intervalTemplates;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchIntervalTemplates() {
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.intervalTemplate
|
||||||
|
);
|
||||||
|
intervalTemplates.value = response.documents.map(
|
||||||
|
(d: Models.Document): IntervalTemplate => {
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
timeTuples: arrayToTimeTuples(d.timeTuple),
|
||||||
|
} as IntervalTemplate;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch timeblock templates', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createIntervalTemplate = async (template: IntervalTemplate) => {
|
||||||
|
try {
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.intervalTemplate,
|
||||||
|
ID.unique(),
|
||||||
|
{ name: template.name, timeTuple: template.timeTuples.flat(2) }
|
||||||
|
);
|
||||||
|
intervalTemplates.value.push(response as IntervalTemplate);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating IntervalTemplate: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const deleteIntervalTemplate = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.intervalTemplate,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
intervalTemplates.value = intervalTemplates.value.filter(
|
||||||
|
(template) => template.$id !== id
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting IntervalTemplate: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updateIntervalTemplate = async (
|
||||||
|
template: IntervalTemplate,
|
||||||
|
id: string
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.intervalTemplate,
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
name: template.name,
|
||||||
|
timeTuple: template.timeTuples.flat(2),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
intervalTemplates.value = intervalTemplates.value.map((b) =>
|
||||||
|
b.$id !== id
|
||||||
|
? b
|
||||||
|
: ({
|
||||||
|
...response,
|
||||||
|
timeTuples: arrayToTimeTuples(response.timeTuple),
|
||||||
|
} as IntervalTemplate)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating IntervalTemplate: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getIntervalTemplates,
|
||||||
|
fetchIntervalTemplates,
|
||||||
|
createIntervalTemplate,
|
||||||
|
deleteIntervalTemplate,
|
||||||
|
updateIntervalTemplate,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type { Reservation } from './schedule.types';
|
import type { Reservation } from './schedule.types';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
||||||
import { ID, Query } from 'appwrite';
|
import { ID, Query } from 'appwrite';
|
||||||
import { date } from 'quasar';
|
import { date, useQuasar } from 'quasar';
|
||||||
import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
|
import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
|
||||||
import { LoadingTypes } from 'src/utils/misc';
|
import { LoadingTypes } from 'src/utils/misc';
|
||||||
import { useAuthStore } from './auth';
|
import { useAuthStore } from './auth';
|
||||||
|
import { isPast } from 'src/utils/schedule';
|
||||||
|
|
||||||
export const useReservationStore = defineStore('reservation', () => {
|
export const useReservationStore = defineStore('reservation', () => {
|
||||||
const reservations = ref<Map<string, Reservation>>(new Map());
|
const reservations = ref<Map<string, Reservation>>(new Map());
|
||||||
const datesLoaded = ref<Record<string, LoadingTypes>>({});
|
const datesLoaded = ref<Record<string, LoadingTypes>>({});
|
||||||
const userReservations = ref<Reservation[]>();
|
const userReservations = ref<Map<string, Reservation>>(new Map());
|
||||||
|
// TODO: Come up with a better way of storing reservations by date & reservations for user
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
// Fetch reservations for a specific date range
|
// Fetch reservations for a specific date range
|
||||||
const fetchReservationsForDateRange = async (
|
const fetchReservationsForDateRange = async (
|
||||||
@@ -45,18 +48,46 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
setDateLoaded(startDate, endDate, 'error');
|
setDateLoaded(startDate, endDate, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const createReservation = async (reservation: Reservation) => {
|
const getReservationById = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await databases.createDocument(
|
const response = await databases.getDocument(
|
||||||
AppwriteIds.databaseId,
|
AppwriteIds.databaseId,
|
||||||
AppwriteIds.collection.reservation,
|
AppwriteIds.collection.reservation,
|
||||||
ID.unique(),
|
id
|
||||||
reservation
|
|
||||||
);
|
);
|
||||||
|
return response as Reservation;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch reservation: ', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createOrUpdateReservation = async (
|
||||||
|
reservation: Reservation
|
||||||
|
): Promise<Reservation> => {
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
if (reservation.$id) {
|
||||||
|
response = await databases.updateDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.reservation,
|
||||||
|
reservation.$id,
|
||||||
|
reservation
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
response = await databases.createDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.reservation,
|
||||||
|
ID.unique(),
|
||||||
|
reservation
|
||||||
|
);
|
||||||
|
}
|
||||||
reservations.value.set(response.$id, response as Reservation);
|
reservations.value.set(response.$id, response as Reservation);
|
||||||
|
userReservations.value.set(response.$id, response as Reservation);
|
||||||
console.info('Reservation booked: ', response);
|
console.info('Reservation booked: ', response);
|
||||||
|
return response as Reservation;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error creating Reservation: ' + e);
|
console.error('Error creating Reservation: ' + e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,16 +104,40 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const status = $q.notify({
|
||||||
|
color: 'secondary',
|
||||||
|
textColor: 'white',
|
||||||
|
message: 'Deleting Reservation',
|
||||||
|
spinner: true,
|
||||||
|
closeBtn: 'Dismiss',
|
||||||
|
position: 'top',
|
||||||
|
timeout: 0,
|
||||||
|
group: false,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await databases.deleteDocument(
|
await databases.deleteDocument(
|
||||||
AppwriteIds.databaseId,
|
AppwriteIds.databaseId,
|
||||||
AppwriteIds.collection.interval,
|
AppwriteIds.collection.reservation,
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
reservations.value.delete(id);
|
reservations.value.delete(id);
|
||||||
|
userReservations.value.delete(id);
|
||||||
console.info(`Deleted reservation: ${id}`);
|
console.info(`Deleted reservation: ${id}`);
|
||||||
|
status({
|
||||||
|
color: 'warning',
|
||||||
|
message: 'Reservation Deleted',
|
||||||
|
spinner: false,
|
||||||
|
icon: 'delete',
|
||||||
|
timeout: 4000,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error deleting reservation: ' + e);
|
console.error('Error deleting reservation: ' + e);
|
||||||
|
status({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Failed to Delete Reservation',
|
||||||
|
spinner: false,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,10 +220,6 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserReservations = () => {
|
|
||||||
return userReservations;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUserReservations = async () => {
|
const fetchUserReservations = async () => {
|
||||||
if (!authStore.currentUser) return;
|
if (!authStore.currentUser) return;
|
||||||
try {
|
try {
|
||||||
@@ -177,21 +228,64 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
AppwriteIds.collection.reservation,
|
AppwriteIds.collection.reservation,
|
||||||
[Query.equal('user', authStore.currentUser.$id)]
|
[Query.equal('user', authStore.currentUser.$id)]
|
||||||
);
|
);
|
||||||
userReservations.value = response.documents as Reservation[];
|
response.documents.forEach((d) =>
|
||||||
|
userReservations.value.set(d.$id, d as Reservation)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch reservations for user: ', error);
|
console.error('Failed to fetch reservations for user: ', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sortedUserReservations = computed((): Reservation[] =>
|
||||||
|
[...userReservations.value?.values()].sort(
|
||||||
|
(a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const futureUserReservations = computed((): Reservation[] => {
|
||||||
|
if (!sortedUserReservations.value) return [];
|
||||||
|
return sortedUserReservations.value.filter((b) => !isPast(b.end));
|
||||||
|
});
|
||||||
|
|
||||||
|
const pastUserReservations = computed((): Reservation[] => {
|
||||||
|
if (!sortedUserReservations.value) return [];
|
||||||
|
return sortedUserReservations.value?.filter((b) => isPast(b.end));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure reactivity for computed properties when Map is modified
|
||||||
|
watch(
|
||||||
|
reservations,
|
||||||
|
() => {
|
||||||
|
sortedUserReservations.value;
|
||||||
|
futureUserReservations.value;
|
||||||
|
pastUserReservations.value;
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
userReservations,
|
||||||
|
() => {
|
||||||
|
sortedUserReservations.value;
|
||||||
|
futureUserReservations.value;
|
||||||
|
pastUserReservations.value;
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getReservationsByDate,
|
getReservationsByDate,
|
||||||
createReservation,
|
getReservationById,
|
||||||
|
createOrUpdateReservation,
|
||||||
deleteReservation,
|
deleteReservation,
|
||||||
fetchReservationsForDateRange,
|
fetchReservationsForDateRange,
|
||||||
isReservationOverlapped,
|
isReservationOverlapped,
|
||||||
isResourceTimeOverlapped,
|
isResourceTimeOverlapped,
|
||||||
getConflictingReservations,
|
getConflictingReservations,
|
||||||
fetchUserReservations,
|
fetchUserReservations,
|
||||||
getUserReservations,
|
sortedUserReservations,
|
||||||
|
futureUserReservations,
|
||||||
|
pastUserReservations,
|
||||||
|
userReservations,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function getSampleIntervals(): Interval[] {
|
|||||||
return template.blocks.map((t: TimeTuple): Interval => {
|
return template.blocks.map((t: TimeTuple): Interval => {
|
||||||
return {
|
return {
|
||||||
$id: 'id' + Math.random().toString(32).slice(2),
|
$id: 'id' + Math.random().toString(32).slice(2),
|
||||||
boatId: b.$id,
|
resource: b.$id,
|
||||||
start: addToDate(tsToday, { day: i }).date + ' ' + t[0],
|
start: addToDate(tsToday, { day: i }).date + ' ' + t[0],
|
||||||
end: addToDate(tsToday, { day: i }).date + ' ' + t[1],
|
end: addToDate(tsToday, { day: i }).date + ' ' + t[1],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import { Models } from 'appwrite';
|
|||||||
import { LoadingTypes } from 'src/utils/misc';
|
import { LoadingTypes } from 'src/utils/misc';
|
||||||
|
|
||||||
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
||||||
export type Reservation = Partial<Models.Document> & {
|
export type Reservation = Interval & {
|
||||||
user: string;
|
user: string;
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
resource: string; // Boat ID
|
|
||||||
status?: StatusTypes;
|
status?: StatusTypes;
|
||||||
reason: string;
|
reason: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
members?: string[];
|
||||||
|
guests?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 24 hrs in advance only 2 weekday, and 1 weekend slot
|
// 24 hrs in advance only 2 weekday, and 1 weekend slot
|
||||||
@@ -19,11 +18,11 @@ export type Reservation = Partial<Models.Document> & {
|
|||||||
objects in here? */
|
objects in here? */
|
||||||
|
|
||||||
export type TimeTuple = [start: string, end: string];
|
export type TimeTuple = [start: string, end: string];
|
||||||
|
|
||||||
export type Interval = Partial<Models.Document> & {
|
export type Interval = Partial<Models.Document> & {
|
||||||
boatId: string;
|
resource: string;
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
selected?: false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IntervalTemplate = Partial<Models.Document> & {
|
export type IntervalTemplate = Partial<Models.Document> & {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function timeTuplesOverlapped(tuples: TimeTuple[]): Interval[] {
|
|||||||
return intervalsOverlapped(
|
return intervalsOverlapped(
|
||||||
tuples.map((tuples) => {
|
tuples.map((tuples) => {
|
||||||
return {
|
return {
|
||||||
boatId: '',
|
resource: '',
|
||||||
start: '01/01/2001 ' + tuples[0],
|
start: '01/01/2001 ' + tuples[0],
|
||||||
end: '01/01/2001 ' + tuples[1],
|
end: '01/01/2001 ' + tuples[1],
|
||||||
};
|
};
|
||||||
@@ -64,14 +64,27 @@ export function buildInterval(
|
|||||||
/* When the time zone offset is absent, date-only forms are interpreted
|
/* When the time zone offset is absent, date-only forms are interpreted
|
||||||
as a UTC time and date-time forms are interpreted as local time. */
|
as a UTC time and date-time forms are interpreted as local time. */
|
||||||
const result = {
|
const result = {
|
||||||
boatId: resource.$id,
|
resource: resource.$id,
|
||||||
start: new Date(blockDate + 'T' + time[0]).toISOString(),
|
start: new Date(blockDate + 'T' + time[0]).toISOString(),
|
||||||
end: new Date(blockDate + 'T' + time[1]).toISOString(),
|
end: new Date(blockDate + 'T' + time[1]).toISOString(),
|
||||||
};
|
};
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isPast = (itemDate: Date | string): boolean => {
|
||||||
|
if (!(itemDate instanceof Date)) {
|
||||||
|
itemDate = new Date(itemDate);
|
||||||
|
}
|
||||||
|
const currentDate = new Date();
|
||||||
|
return itemDate < currentDate;
|
||||||
|
};
|
||||||
|
|
||||||
export function formatDate(inputDate: string | undefined): string {
|
export function formatDate(inputDate: string | undefined): string {
|
||||||
if (!inputDate) return '';
|
if (!inputDate) return '';
|
||||||
return date.formatDate(new Date(inputDate), 'ddd MMM Do hh:mm A');
|
return date.formatDate(new Date(inputDate), 'ddd MMM Do hh:mm A');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTime(inputDate: string | undefined): string {
|
||||||
|
if (!inputDate) return '';
|
||||||
|
return date.formatDate(new Date(inputDate), 'hh:mm A');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user