This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/components/input/datepicker.vue

300 lines
6.7 KiB
Vue

<template>
<div class="datepicker" ref="datepicker">
<BaseButton @click="toggleDatePopup" class="show" :disabled="disabled || undefined">
{{ state.date === null ? chooseDateLabel : formatDateShort(state.date) }}
</BaseButton>
<transition name="fade">
<div v-if="show" class="datepicker-popup">
<BaseButton
v-for="({dayInterval, label, icon}) of selectDateOptions"
:key="dayInterval"
class="datepicker__quick-select-date"
@click="setDate(dayInterval)"
>
<span class="icon"><icon :icon="icon"/></span>
<span class="text">
<span>{{ label }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval(dayInterval) }}</span>
</span>
</BaseButton>
<flat-pickr
:config="flatPickerConfig"
class="input"
v-model="flatPickrDate"
/>
<x-button
class="datepicker__close-button"
:shadow="false"
@click="closeDatePopup"
v-cy="'closeDatepicker'"
>
{{ $t('misc.confirm') }}
</x-button>
</div>
</transition>
</div>
</template>
<script lang="ts" setup>
import {ref, reactive, computed, watch, type PropType } from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from 'vuex'
import {useNow, onClickOutside} from '@vueuse/core'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import BaseButton from '@/components/base/BaseButton.vue'
import {format} from 'date-fns'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {createDateFromString} from '@/helpers/time/createDateFromString'
import {formatDateShort} from '@/helpers/time/formatDate'
function getDateFromDayInterval(dayInterval: string) {
const interval = calculateDayInterval(dayInterval)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0)
newDate.setSeconds(0)
return newDate
}
function getWeekdayFromStringInterval(dateInterval: string) {
const interval = calculateDayInterval(dateInterval)
const newDate = new Date(now.value)
newDate.setDate(newDate.getDate() + interval)
return format(newDate, 'E')
}
const props = defineProps({
// FIXME: should only accept Date objects. Then we wouldn't need all the conversion
modelValue: {
type: [Date, null, String] as PropType<Date | null | string>,
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
default: null,
},
chooseDateLabel: {
type: String as PropType<string>,
default() {
const {t} = useI18n({useScope: 'global'})
return t('input.datepicker.chooseDate')
},
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const {t} = useI18n({useScope: 'global'})
const store = useStore()
const show = ref(false)
// FIXME: replace with popup
const datepicker = ref<HTMLElement | null>(null)
onClickOutside(datepicker, closeDatePopup)
function toggleDatePopup() {
if (props.disabled) {
return
}
show.value = !show.value
}
function closeDatePopup() {
show.value = false
}
const state : {
date: Date | null,
initialDate: Date | null,
}= reactive({
date: null,
initialDate: null,
})
// const date = ref<Date | null>(null)
// const initialDate = ref<Date | null>(null)
watch(
() => props.modelValue,
(value: null | Date | string) => {
const newDate = value === null ? null : createDateFromString(value)
Object.assign(state, {
date: newDate,
initialDate: newDate,
})
},
{ immediate: true },
)
function isSameDate(dateA: Date | null, dateB: Date | null) {
return (dateA !== null ? new Date(dateA).toISOString() : null) ===
(dateB !== null ? new Date(dateB).toISOString() : null)
}
watch(show, (newIsShown) => {
newIsShown && console.log(state.date, state.initialDate)
if (
newIsShown === false &&
!isSameDate(state.initialDate, state.date)
) {
console.log('initialDate', state.initialDate !== null ? new Date(state.initialDate).toISOString() : null)
console.log('date', state.date !== null ? new Date(state.date).toISOString() : null)
// make copy of date
emit('update:modelValue', state.date !== null ? new Date(state.date) : null)
}
})
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: store.state.auth.settings.weekStart,
},
}))
// Since flatpickr dates are strings, we need to convert them to native date objects.
// To make that work, we need a separate variable since flatpickr does not have a change event.
const flatPickrDate = computed({
get() {
if (!state.date) {
return ''
}
return format(state.date, 'yyy-LL-dd H:mm')
},
set(newValue: string) {
state.date = createDateFromString(newValue)
},
})
const now = useNow()
const selectDateOptions = computed(() => {
const options = [
{
condition: now.value.getHours() < 21,
dayInterval: 'today',
icon: ['far', 'calendar-alt'],
label: t('input.datepicker.today'),
},
{
dayInterval: 'tomorrow',
icon: ['far', 'sun'],
label: t('input.datepicker.tomorrow'),
},
{
dayInterval: 'nextMonday',
icon: 'coffee',
label: t('input.datepicker.nextMonday'),
},
{
dayInterval: 'thisWeekend',
icon: 'cocktail',
label: t('input.datepicker.thisWeekend'),
},
{
dayInterval: 'laterThisWeek',
icon: 'chess-knight',
label: t('input.datepicker.laterThisWeek'),
},
{
dayInterval: 'nextWeek',
icon: 'forward',
label: t('input.datepicker.nextWeek'),
},
]
return options.filter((option) => option.condition || option.condition === undefined)
})
function setDate(dayInterval: string) {
state.date = getDateFromDayInterval(dayInterval)
}
</script>
<style lang="scss" scoped>
.datepicker {
input.input {
display: none;
}
}
.datepicker-popup {
position: absolute;
z-index: 99;
width: 320px;
background: var(--white);
border-radius: $radius;
box-shadow: $shadow;
@media screen and (max-width: ($tablet)) {
width: calc(100vw - 5rem);
}
}
.datepicker__quick-select-date {
display: flex;
align-items: center;
padding: 0 .5rem;
width: 100%;
height: 2.25rem;
color: var(--text);
transition: all $transition;
&:first-child {
border-radius: $radius $radius 0 0;
}
&:hover {
background: var(--grey-100);
}
.text {
width: 100%;
font-size: .85rem;
display: flex;
justify-content: space-between;
padding-right: .25rem;
.weekday {
color: var(--text-light);
text-transform: capitalize;
}
}
.icon {
width: 2rem;
text-align: center;
}
}
.datepicker__close-button {
margin: 1rem;
width: calc(100% - 2rem);
}
:deep(.flatpickr-calendar) {
margin: 0 auto 8px;
box-shadow: none;
}
</style>