All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m1s
241 lines
6.2 KiB
Vue
241 lines
6.2 KiB
Vue
<!-- This has been abandoned for now. Going to block-based booking. Will probably need the schedule viewer functionality at some point in the future, though -->
|
|
<template>
|
|
<q-card-section>
|
|
<div class="text-caption text-justify">
|
|
Use the calendar to pick a date. Tap a box in the grid for the boat and
|
|
start time. Select the duration below.
|
|
</div>
|
|
<div style="width: 100%; display: flex; justify-content: center">
|
|
<div
|
|
style="
|
|
width: 50%;
|
|
max-width: 350px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
"
|
|
>
|
|
<span
|
|
class="q-button"
|
|
style="cursor: pointer; user-select: none"
|
|
@click="onPrev"
|
|
><</span
|
|
>
|
|
{{ formattedMonth }}
|
|
<span
|
|
class="q-button"
|
|
style="cursor: pointer; user-select: none"
|
|
@click="onNext"
|
|
>></span
|
|
>
|
|
</div>
|
|
</div>
|
|
<div
|
|
style="
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-wrap: nowrap;
|
|
"
|
|
>
|
|
<div style="display: flex; width: 100%">
|
|
<q-calendar-month
|
|
ref="calendar"
|
|
v-model="selectedDate"
|
|
:disabled-before="disabledBefore"
|
|
animated
|
|
bordered
|
|
mini-mode
|
|
date-type="rounded"
|
|
@change="onChange"
|
|
@moved="onMoved"
|
|
@click-date="onClickDate"
|
|
/>
|
|
</div></div
|
|
></q-card-section>
|
|
<q-calendar-resource
|
|
v-model="selectedDate"
|
|
:model-resources="boatStore.boats"
|
|
resource-key="id"
|
|
resource-label="displayName"
|
|
resource-width="32"
|
|
:interval-start="6"
|
|
:interval-count="18"
|
|
:interval-minutes="60"
|
|
cell-width="48"
|
|
style="--calendar-resources-width: 48px"
|
|
resource-min-height="40"
|
|
animated
|
|
bordered
|
|
@change="onChange"
|
|
@moved="onMoved"
|
|
@resource-expanded="onResourceExpanded"
|
|
@click-date="onClickDate"
|
|
@click-time="onClickTime"
|
|
@click-resource="onClickResource"
|
|
@click-head-resources="onClickHeadResources"
|
|
@click-interval="onClickInterval"
|
|
>
|
|
<template #resource-intervals="{ scope }">
|
|
<template v-for="(event, index) in getEvents(scope)" :key="index">
|
|
<q-badge outline :label="event.title" :style="getStyle(event)" />
|
|
</template>
|
|
</template>
|
|
|
|
<template #resource-label="{ scope: { resource } }">
|
|
<div class="col-12 .col-md-auto">
|
|
{{ resource.displayName }}
|
|
<q-icon v-if="resource.defects" name="warning" color="warning" />
|
|
</div>
|
|
</template>
|
|
</q-calendar-resource>
|
|
|
|
<q-card-section>
|
|
<q-select
|
|
filled
|
|
v-model="duration"
|
|
:options="durations"
|
|
dense
|
|
@update:model-value="onUpdateDuration"
|
|
label="Duration (hours)"
|
|
stack-label
|
|
><template v-slot:append><q-icon name="timelapse" /></template></q-select
|
|
></q-card-section>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import {
|
|
QCalendarResource,
|
|
TimestampOrNull,
|
|
today,
|
|
parseDate,
|
|
parseTimestamp,
|
|
addToDate,
|
|
Timestamp,
|
|
} from '@quasar/quasar-ui-qcalendar';
|
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
|
import { useScheduleStore } from 'src/stores/schedule';
|
|
import { date } from 'quasar';
|
|
import { computed } from 'vue';
|
|
import type { StatusTypes } from 'src/stores/schedule';
|
|
|
|
interface EventData {
|
|
event: object;
|
|
scope: {
|
|
timestamp: object;
|
|
columnindex: number;
|
|
activeDate: boolean;
|
|
droppable: boolean;
|
|
};
|
|
}
|
|
|
|
const durations = [1, 1.5, 2, 2.5, 3, 3.5, 4];
|
|
|
|
interface ResourceIntervalScope {
|
|
resource: Boat;
|
|
intervals: [];
|
|
timeStartPosX(start: TimestampOrNull): number;
|
|
timeDurationWidth(duration: number): number;
|
|
}
|
|
|
|
const statusLookup = {
|
|
confirmed: ['#14539a', 'white'],
|
|
pending: ['#f2c037', 'white'],
|
|
tentative: ['white', 'grey'],
|
|
};
|
|
|
|
const calendar = ref();
|
|
const boatStore = useBoatStore();
|
|
const scheduleStore = useScheduleStore();
|
|
const selectedDate = ref(today());
|
|
const duration = ref(1);
|
|
|
|
const formattedMonth = computed(() => {
|
|
const date = new Date(selectedDate.value);
|
|
|
|
return monthFormatter()?.format(date);
|
|
});
|
|
const disabledBefore = computed(() => {
|
|
const todayTs = parseTimestamp(today()) as Timestamp;
|
|
return addToDate(todayTs, { day: -1 }).date;
|
|
});
|
|
|
|
function monthFormatter() {
|
|
try {
|
|
return new Intl.DateTimeFormat('en-CA' || undefined, {
|
|
month: 'long',
|
|
timeZone: 'UTC',
|
|
});
|
|
} catch (e) {
|
|
//
|
|
}
|
|
}
|
|
|
|
function getEvents(scope: ResourceIntervalScope) {
|
|
const resourceEvents = scheduleStore.getBoatReservations(
|
|
date.extractDate(selectedDate.value, 'YYYY-MM-DD'),
|
|
scope.resource.$id
|
|
);
|
|
|
|
return resourceEvents.map((event) => {
|
|
return {
|
|
left: scope.timeStartPosX(parseDate(event.start)),
|
|
width: scope.timeDurationWidth(
|
|
date.getDateDiff(event.end, event.start, 'minutes')
|
|
),
|
|
title: event.user,
|
|
status: event.status,
|
|
};
|
|
});
|
|
}
|
|
|
|
function getStyle(event: {
|
|
left: number;
|
|
width: number;
|
|
title: string;
|
|
status: StatusTypes;
|
|
}) {
|
|
return {
|
|
position: 'absolute',
|
|
background: event.status ? statusLookup[event.status][0] : 'white',
|
|
color: event.status ? statusLookup[event.status][1] : '#14539a',
|
|
left: `${event.left}px`,
|
|
width: `${event.width}px`,
|
|
height: '32px',
|
|
overflow: 'hidden',
|
|
};
|
|
}
|
|
|
|
const emit = defineEmits(['onClickTime', 'onUpdateDuration']);
|
|
|
|
function onPrev() {
|
|
calendar.value.prev();
|
|
}
|
|
function onNext() {
|
|
calendar.value.next();
|
|
}
|
|
|
|
function onClickDate(data: EventData) {
|
|
return data;
|
|
}
|
|
|
|
function onClickTime(data: EventData) {
|
|
// TODO: Add a duration picker, here.
|
|
emit('onClickTime', data);
|
|
}
|
|
function onUpdateDuration(value: EventData) {
|
|
emit('onUpdateDuration', value);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
const onClickInterval = () => {};
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
const onClickHeadResources = () => {};
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
const onClickResource = () => {};
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
const onResourceExpanded = () => {};
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
const onMoved = () => {};
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
const onChange = () => {};
|
|
</script>
|