refactor: everything to nuxt.js
This commit is contained in:
236
app/components/scheduling/boat/BoatScheduleTableComponent.vue
Normal file
236
app/components/scheduling/boat/BoatScheduleTableComponent.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<script setup lang="ts">
|
||||
import type { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
||||
import {
|
||||
QCalendarDay,
|
||||
diffTimestamp,
|
||||
today,
|
||||
parseTimestamp,
|
||||
parseDate,
|
||||
addToDate,
|
||||
} from '@quasar/quasar-ui-qcalendar';
|
||||
import CalendarHeaderComponent from './CalendarHeaderComponent.vue';
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useBoatStore } from '~/stores/boat';
|
||||
import { useAuthStore } from '~/stores/auth';
|
||||
import type { Interval, Reservation } from '~/utils/schedule.types';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useReservationStore } from '~/stores/reservation';
|
||||
import { useIntervalTemplateStore } from '~/stores/intervalTemplate';
|
||||
import { useIntervalStore } from '~/stores/interval';
|
||||
|
||||
const intervalTemplateStore = useIntervalTemplateStore();
|
||||
const reservationStore = useReservationStore();
|
||||
const { boats } = storeToRefs(useBoatStore());
|
||||
const selectedBlock = defineModel<Interval | null>();
|
||||
const selectedDate = ref(today());
|
||||
const { getAvailableIntervals } = useIntervalStore();
|
||||
const calendar = ref<typeof QCalendarDay | null>(null);
|
||||
const now = ref(new Date());
|
||||
let intervalId: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
onMounted(async () => {
|
||||
await useBoatStore().fetchBoats();
|
||||
await intervalTemplateStore.fetchIntervalTemplates();
|
||||
intervalId = setInterval(() => { now.value = new Date(); }, 60000);
|
||||
});
|
||||
|
||||
onUnmounted(() => clearInterval(intervalId));
|
||||
|
||||
function handleSwipe({ direction }: { direction: string }) {
|
||||
if (direction === 'right') {
|
||||
calendar.value?.prev();
|
||||
} else {
|
||||
calendar.value?.next();
|
||||
}
|
||||
}
|
||||
|
||||
function reservationStyles(
|
||||
reservation: Reservation,
|
||||
timeStartPos: (t: string) => string,
|
||||
timeDurationHeight: (d: number) => string
|
||||
) {
|
||||
return genericBlockStyle(
|
||||
parseDate(new Date(reservation.start)) as Timestamp,
|
||||
parseDate(new Date(reservation.end)) as Timestamp,
|
||||
timeStartPos,
|
||||
timeDurationHeight
|
||||
);
|
||||
}
|
||||
|
||||
function getUserName(userid: string) {
|
||||
return useAuthStore().getUserNameById(userid);
|
||||
}
|
||||
|
||||
function blockStyles(
|
||||
block: Interval,
|
||||
timeStartPos: (t: string) => string,
|
||||
timeDurationHeight: (d: number) => string
|
||||
) {
|
||||
return genericBlockStyle(
|
||||
parseDate(new Date(block.start)) as Timestamp,
|
||||
parseDate(new Date(block.end)) as Timestamp,
|
||||
timeStartPos,
|
||||
timeDurationHeight
|
||||
);
|
||||
}
|
||||
|
||||
function getBoatDisplayName(scope: DayBodyScope) {
|
||||
return boats.value[scope.columnIndex]?.displayName ?? '';
|
||||
}
|
||||
|
||||
function beforeNow(time: Date) {
|
||||
return time < now.value || null;
|
||||
}
|
||||
|
||||
function genericBlockStyle(
|
||||
start: Timestamp,
|
||||
end: Timestamp,
|
||||
timeStartPos: (t: string) => string,
|
||||
timeDurationHeight: (d: number) => string
|
||||
) {
|
||||
const s = { top: '', height: '', opacity: '' };
|
||||
if (timeStartPos && timeDurationHeight) {
|
||||
s.top = timeStartPos(start.time) + 'px';
|
||||
s.height =
|
||||
parseInt(timeDurationHeight(diffTimestamp(start, end, false) / 1000 / 60)) - 1 + 'px';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
interface DayBodyScope {
|
||||
columnIndex: number;
|
||||
timeDurationHeight: string;
|
||||
timeStartPos: (time: string, clamp: boolean) => string;
|
||||
timestamp: Timestamp;
|
||||
}
|
||||
|
||||
function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Interval) {
|
||||
if (scope.timestamp.disabled || new Date(block.end) < new Date()) return false;
|
||||
selectedBlock.value = block;
|
||||
}
|
||||
|
||||
const boatReservations = computed((): Record<string, Reservation[]> => {
|
||||
return reservationStore
|
||||
.getReservationsByDate(selectedDate.value)
|
||||
.value.reduce((result, reservation) => {
|
||||
if (!result[reservation.resource]) result[reservation.resource] = [];
|
||||
result[reservation.resource]!.push(reservation);
|
||||
return result;
|
||||
}, <Record<string, Reservation[]>>{});
|
||||
});
|
||||
|
||||
function getBoatReservations(scope: DayBodyScope): Reservation[] {
|
||||
const boat = boats.value[scope.columnIndex];
|
||||
return boat ? boatReservations.value[boat.$id] ?? [] : [];
|
||||
}
|
||||
|
||||
const disabledBefore = computed(() => {
|
||||
const todayTs = parseTimestamp(today()) as Timestamp;
|
||||
return addToDate(todayTs, { day: -1 }).date;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-card>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Select a Boat and Time</q-toolbar-title>
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-toolbar>
|
||||
<q-separator />
|
||||
<CalendarHeaderComponent v-model="selectedDate" />
|
||||
<div class="boat-schedule-table-component">
|
||||
<QCalendarDay
|
||||
ref="calendar"
|
||||
class="q-pa-xs"
|
||||
flat
|
||||
animated
|
||||
dense
|
||||
:disabled-before="disabledBefore"
|
||||
interval-height="24"
|
||||
interval-count="18"
|
||||
interval-start="06:00"
|
||||
:short-interval-label="true"
|
||||
v-model="selectedDate"
|
||||
:column-count="boats.length"
|
||||
v-touch-swipe.left.right="handleSwipe">
|
||||
<template #head-day="{ scope }">
|
||||
<div style="text-align: center; font-weight: 800">
|
||||
{{ getBoatDisplayName(scope) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #day-body="{ scope }">
|
||||
<div
|
||||
v-for="block in getAvailableIntervals(scope.timestamp, boats[scope.columnIndex]).value"
|
||||
:key="block.$id">
|
||||
<div
|
||||
class="timeblock"
|
||||
:disabled="beforeNow(new Date(block.end))"
|
||||
:class="selectedBlock?.$id === block.$id ? 'selected' : ''"
|
||||
:style="blockStyles(block, scope.timeStartPos, scope.timeDurationHeight)"
|
||||
:id="block.$id"
|
||||
@click="selectBlock($event, scope, block)">
|
||||
{{ boats[scope.columnIndex]?.name }}<br />
|
||||
{{ selectedBlock?.$id === block.$id ? 'Selected' : 'Available' }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="reservation in getBoatReservations(scope)" :key="reservation.$id">
|
||||
<div
|
||||
class="reservation column"
|
||||
:style="reservationStyles(reservation, scope.timeStartPos, scope.timeDurationHeight)">
|
||||
{{ getUserName(reservation.user) || 'loading...' }}<br />
|
||||
<q-chip class="gt-md">{{ reservation.reason }}</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</QCalendarDay>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.boat-schedule-table-component
|
||||
display: flex
|
||||
max-height: 60vh
|
||||
max-width: 98vw
|
||||
.reservation
|
||||
display: flex
|
||||
position: absolute
|
||||
justify-content: center
|
||||
align-items: center
|
||||
text-align: center
|
||||
width: 100%
|
||||
opacity: 1
|
||||
margin: 0px
|
||||
text-overflow: ellipsis
|
||||
font-size: 0.8em
|
||||
cursor: pointer
|
||||
background: $accent
|
||||
color: white
|
||||
border: 1px solid black
|
||||
.timeblock
|
||||
display: flex
|
||||
position: absolute
|
||||
justify-content: center
|
||||
text-align: center
|
||||
align-items: center
|
||||
width: 100%
|
||||
opacity: 0.5
|
||||
margin: 0px
|
||||
text-overflow: ellipsis
|
||||
font-size: 0.8em
|
||||
cursor: pointer
|
||||
background: $primary
|
||||
color: white
|
||||
border: 1px solid black
|
||||
.selected
|
||||
opacity: 1 !important
|
||||
.q-calendar-day__interval--text
|
||||
font-size: 0.8em
|
||||
.q-calendar-day__day.q-current-day
|
||||
padding: 1px
|
||||
.q-calendar-day__head--days__column
|
||||
background: $primary
|
||||
color: white
|
||||
</style>
|
||||
192
app/components/scheduling/boat/CalendarHeaderComponent.vue
Normal file
192
app/components/scheduling/boat/CalendarHeaderComponent.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import type { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
||||
import {
|
||||
addToDate,
|
||||
createDayList,
|
||||
createNativeLocaleFormatter,
|
||||
getEndOfWeek,
|
||||
getStartOfWeek,
|
||||
parseTimestamp,
|
||||
today,
|
||||
} from '@quasar/quasar-ui-qcalendar';
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
|
||||
const selectedDate = defineModel<string>();
|
||||
|
||||
const weekdays = reactive([1, 2, 3, 4, 5, 6, 0]);
|
||||
const locale = ref('en-CA');
|
||||
const monthFormatter = monthFormatterFunc();
|
||||
const dayFormatter = dayFormatterFunc();
|
||||
const weekdayFormatter = weekdayFormatterFunc();
|
||||
|
||||
const today2 = computed(() => parseTimestamp(today()));
|
||||
|
||||
const parsedStart = computed(() =>
|
||||
getStartOfWeek(
|
||||
parseTimestamp(selectedDate.value || today()) as Timestamp,
|
||||
weekdays,
|
||||
today2.value as Timestamp
|
||||
)
|
||||
);
|
||||
|
||||
const parsedEnd = computed(() =>
|
||||
getEndOfWeek(
|
||||
parseTimestamp(selectedDate.value || today()) as Timestamp,
|
||||
weekdays,
|
||||
today2.value as Timestamp
|
||||
)
|
||||
);
|
||||
|
||||
const days = computed(() => {
|
||||
if (parsedStart.value && parsedEnd.value) {
|
||||
return createDayList(parsedStart.value, parsedEnd.value, today2.value as Timestamp, weekdays);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const dayStyle = computed(() => ({ width: 100 / weekdays.length + '%' }));
|
||||
|
||||
function onPrev() {
|
||||
selectedDate.value = addToDate(parsedStart.value, { day: -7 }).date;
|
||||
}
|
||||
|
||||
function onNext() {
|
||||
selectedDate.value = addToDate(parsedStart.value, { day: 7 }).date;
|
||||
}
|
||||
|
||||
function dayClass(day: Timestamp) {
|
||||
return { 'date-button': true, 'selected-date-button': selectedDate.value === day.date };
|
||||
}
|
||||
|
||||
function monthFormatterFunc() {
|
||||
const longOptions: Intl.DateTimeFormatOptions = { timeZone: 'UTC', month: 'long' };
|
||||
const shortOptions: Intl.DateTimeFormatOptions = { timeZone: 'UTC', month: 'short' };
|
||||
return createNativeLocaleFormatter(locale.value, (_tms, short) => short ? shortOptions : longOptions);
|
||||
}
|
||||
|
||||
function weekdayFormatterFunc() {
|
||||
const longOptions: Intl.DateTimeFormatOptions = { timeZone: 'UTC', weekday: 'long' };
|
||||
const shortOptions: Intl.DateTimeFormatOptions = { timeZone: 'UTC', weekday: 'short' };
|
||||
return createNativeLocaleFormatter(locale.value, (_tms, short) => short ? shortOptions : longOptions);
|
||||
}
|
||||
|
||||
function dayFormatterFunc() {
|
||||
const longOptions: Intl.DateTimeFormatOptions = { timeZone: 'UTC', day: '2-digit' };
|
||||
const shortOptions: Intl.DateTimeFormatOptions = { timeZone: 'UTC', day: 'numeric' };
|
||||
return createNativeLocaleFormatter(locale.value, (_tms, short) => short ? shortOptions : longOptions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="title-bar" style="display: flex">
|
||||
<button tabindex="0" class="date-button direction-button direction-button__left" @click="onPrev">
|
||||
<span class="q-calendar__focus-helper" tabindex="-1" />
|
||||
</button>
|
||||
<div class="dates-holder">
|
||||
<div :key="parsedStart?.date" class="internal-dates-holder">
|
||||
<div v-for="day in days" :key="day.date" :style="dayStyle">
|
||||
<button tabindex="0" style="width: 100%" :class="dayClass(day)" @click="selectedDate = day.date">
|
||||
<span class="q-calendar__focus-helper" tabindex="-1" />
|
||||
<div style="width: 100%; font-size: 0.9em">{{ monthFormatter(day, true) }}</div>
|
||||
<div style="width: 100%; font-size: 1.2em; font-weight: 700">{{ dayFormatter(day, false) }}</div>
|
||||
<div style="width: 100%; font-size: 1em">{{ weekdayFormatter(day, true) }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button tabindex="0" class="date-button direction-button direction-button__right" @click="onNext">
|
||||
<span class="q-calendar__focus-helper" tabindex="-1" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.title-bar
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 70px
|
||||
background: white
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex: 1 0 100%
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
overflow: hidden
|
||||
border-radius: 3px
|
||||
user-select: none
|
||||
margin: 2px 0px 2px
|
||||
|
||||
.dates-holder
|
||||
position: relative
|
||||
width: 100%
|
||||
align-items: center
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
color: #fff
|
||||
overflow: hidden
|
||||
user-select: none
|
||||
|
||||
.internal-dates-holder
|
||||
position: relative
|
||||
width: 100%
|
||||
display: inline-flex
|
||||
flex: 1 1 100%
|
||||
flex-direction: row
|
||||
justify-content: space-between
|
||||
overflow: hidden
|
||||
user-select: none
|
||||
|
||||
.direction-button
|
||||
background: white
|
||||
color: $primary
|
||||
width: 40px
|
||||
max-width: 50px !important
|
||||
|
||||
.direction-button__left
|
||||
&:before
|
||||
content: '<'
|
||||
display: inline-flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
height: 100%
|
||||
font-weight: 900
|
||||
font-size: 3em
|
||||
|
||||
.direction-button__right
|
||||
&:before
|
||||
content: '>'
|
||||
display: inline-flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
height: 100%
|
||||
font-weight: 900
|
||||
font-size: 3em
|
||||
|
||||
.date-button
|
||||
color: $primary
|
||||
background: white
|
||||
z-index: 2
|
||||
height: 100%
|
||||
outline: 0
|
||||
cursor: pointer
|
||||
border-radius: 3px
|
||||
display: inline-flex
|
||||
flex: 1 0 auto
|
||||
flex-direction: column
|
||||
align-items: stretch
|
||||
position: relative
|
||||
border: 0
|
||||
vertical-align: middle
|
||||
padding: 0
|
||||
font-size: 14px
|
||||
line-height: 1.715em
|
||||
text-decoration: none
|
||||
font-weight: 500
|
||||
text-transform: uppercase
|
||||
text-align: center
|
||||
user-select: none
|
||||
|
||||
.selected-date-button
|
||||
color: white !important
|
||||
background: $primary !important
|
||||
</style>
|
||||
Reference in New Issue
Block a user