feat(reminders): add proper time picker for relative dates

This commit is contained in:
kolaente 2023-06-09 13:19:47 +02:00
parent 7b2a688b6e
commit 95487d7569
Signed by untrusted user: konrad
GPG Key ID: F40E70337AB24C9B
7 changed files with 156 additions and 221 deletions

View File

@ -151,7 +151,6 @@ function setDateValue(dateString: string | Date | null) {
function updateData() {
changed.value = true
console.log('emit', date.value)
emit('update:modelValue', date.value)
}

View File

@ -3,29 +3,30 @@
{{ reminderText }}
<div class="presets">
<div class="options" v-if="showFormSwitch === null">
<BaseButton
v-for="p in presets"
>
{{ formatReminder(p) }}
</BaseButton>
<BaseButton>
<BaseButton @click="showFormSwitch = 'relative'">
Custom
</BaseButton>
<BaseButton @click="showFormSwitch = 'absolute'">
Date
</BaseButton>
</div>
<ReminderPeriod
v-if="showRelativeReminder"
v-if="showFormSwitch === 'relative'"
v-model="reminder"
:disabled="disabled"
@update:modelValue="emit('update:modelValue', reminder.value)"
@update:modelValue="emit('update:modelValue', reminder)"
/>
<Datepicker
v-if="showAbsoluteReminder"
<DatepickerInline
v-if="showFormSwitch === 'absolute'"
v-model="reminderDate"
:disabled="disabled"
@close-on-change="setReminderDate"
@update:modelValue="setReminderDate"
/>
</div>
</template>
@ -34,16 +35,17 @@
import {computed, ref, watch, type PropType} from 'vue'
import {toRef} from '@vueuse/core'
import {SECONDS_A_DAY} from '@/constants/date'
import {secondsToPeriod} from '@/helpers/time/period'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import {secondsToPeriod} from '@/helpers/time/period'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import {formatDateShort} from '@/helpers/time/formatDate'
import Datepicker from '@/components/input/datepicker.vue'
import ReminderPeriod from '@/components/tasks/partials/reminder-period.vue'
import TaskReminderModel from '@/models/taskReminder'
import BaseButton from '@/components/base/BaseButton.vue'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import DatepickerInline from '@/components/input/datepickerInline.vue'
import ReminderPeriod from '@/components/tasks/partials/reminder-period.vue'
import TaskReminderModel from '@/models/taskReminder'
const props = defineProps({
modelValue: {
@ -65,20 +67,9 @@ const presets: TaskReminderModel[] = [
{relativePeriod: SECONDS_A_DAY * 7, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE},
{relativePeriod: SECONDS_A_DAY * 30, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE},
]
const reminderDate = computed({
get() {
return reminder.value?.reminder
},
set(newReminderDate) {
if (!reminderDate.value) {
return
}
reminder.value.reminder = new Date(reminderDate.value)
},
})
const reminderDate = ref(null)
const showAbsoluteReminder = computed(() => !reminder.value || !reminder.value?.relativeTo)
const showRelativeReminder = computed(() => !reminder.value || reminder.value?.relativeTo)
const showFormSwitch = ref<null | 'relative' | 'absolute'>(null)
const reminderText = computed(() => {
@ -103,10 +94,9 @@ watch(
)
function setReminderDate() {
if (!reminderDate.value) {
return
}
reminder.value.reminder = new Date(reminderDate.value)
reminder.value.reminder = reminderDate.value === null
? null
: new Date(reminderDate.value)
emit('update:modelValue', reminder.value)
}
@ -123,12 +113,12 @@ function formatReminder(reminder: TaskReminderModel) {
periodHuman = period.days + ' day'
}
return periodHuman + ' ' + (reminder.relativePeriod > 0 ? 'before' : 'after') + ' ' + reminder.relativeTo
return periodHuman + ' ' + (reminder.relativePeriod <= 0 ? 'before' : 'after') + ' ' + reminder.relativeTo
}
</script>
<style lang="scss" scoped>
.presets {
.options {
display: flex;
flex-direction: column;
align-items: flex-start;

View File

@ -1,92 +1,57 @@
<template>
<div
v-if="!!reminder?.relativeTo"
class="reminder-period"
class="reminder-period control"
>
<Popup>
<template #trigger="{toggle}">
<BaseButton
@click="toggle"
:disabled="disabled"
class="show"
>
{{ formatDuration(reminder.relativePeriod) }} {{ reminder.relativePeriod <= 0 ? '&le;' : '&gt;' }}
{{ formatRelativeTo(reminder.relativeTo) }}
<span class="icon"><icon icon="chevron-down"/></span>
</BaseButton>
</template>
<input
class="input"
v-model.number="period.duration"
type="number"
min="0"
@change="updateData"
/>
<template #content>
<div class="mt-2">
<div class="control is-flex is-align-items-center">
<label>
<input
:disabled="disabled"
class="input"
:placeholder="$t('task.reminder.daysShort')"
v-model="periodInput.duration.days"
type="number"
min="0"
/> {{ $t('task.reminder.days') }}
</label>
<input
:disabled="disabled"
class="input"
:placeholder="$t('task.reminder.hoursShort')"
v-model="periodInput.duration.hours"
type="number"
min="0"
/>:
<input
:disabled="disabled"
class="input"
:placeholder="$t('task.reminder.minutesShort')"
v-model="periodInput.duration.minutes"
type="number"
min="0"
/>
<div class="select">
<select v-model="period.durationUnit" @change="updateData">
<option value="minutes">{{ $t('task.reminder.minutes') }}</option>
<option value="hours">{{ $t('task.reminder.hours') }}</option>
<option value="days">{{ $t('task.reminder.days') }}</option>
<option value="weeks">{{ $t('task.reminder.weeks') }}</option>
</select>
</div>
<div class="select">
<select :disabled="disabled" v-model.number="periodInput.sign">
<option value="-1">&le;</option>
<option value="1">&gt;</option>
</select>
</div>
<div class="select">
<select v-model.number="period.sign" @change="updateData">
<option value="-1">
before
</option>
<option value="1">
after
</option>
</select>
</div>
<div class="select">
<select :disabled="disabled" v-model="periodInput.relativeTo">
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE">{{ $t('task.attributes.dueDate') }}</option>
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE">{{ $t('task.attributes.startDate')}}</option>
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE">{{ $t('task.attributes.endDate') }}</option>
</select>
</div>
</div>
<div class="control">
<x-button
:disabled="disabled"
class="close-button"
:shadow="false"
@click="submitForm"
>
{{ $t('misc.confirm') }}
</x-button>
</div>
</div>
</template>
</Popup>
<div class="select">
<select v-model="period.relativeTo" @change="updateData">
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE">
{{ $t('task.attributes.dueDate') }}
</option>
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE">
{{ $t('task.attributes.startDate') }}
</option>
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE">
{{ $t('task.attributes.endDate') }}
</option>
</select>
</div>
</div>
</template>
<script setup lang="ts">
import {reactive, ref, watch, type PropType, computed} from 'vue'
import {ref, watch, type PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import {toRef} from '@vueuse/core'
import BaseButton from '@/components/base/BaseButton.vue'
import Popup from '@/components/misc/popup.vue'
import {periodToSeconds, secondsToPeriod} from '@/helpers/time/period'
import {periodToSeconds, PeriodUnit, secondsToPeriod} from '@/helpers/time/period'
import TaskReminderModel from '@/models/taskReminder'
@ -108,106 +73,55 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const reminder = ref<ITaskReminder>()
const reminder = ref<ITaskReminder>(new TaskReminderModel())
const showForm = ref(false)
const periodInput = reactive({
duration: {
days: 0,
hours: 0,
minutes: 0,
seconds: 0
},
interface PeriodInput {
duration: number,
durationUnit: PeriodUnit,
relativeTo: IReminderPeriodRelativeTo,
sign: -1 | 1,
}
const period = ref<PeriodInput>({
duration: 0,
durationUnit: 'hours',
relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE,
sign: -1,
})
const modelValue = toRef(props, 'modelValue')
watch(
modelValue,
(value) => {
reminder.value = value
if (value && value.relativeTo != null) {
Object.assign(periodInput.duration, secondsToPeriod(Math.abs(value.relativePeriod)))
periodInput.relativeTo = value.relativeTo
periodInput.sign = value.relativePeriod <= 0 ? -1 : 1
} else {
reminder.value = new TaskReminderModel()
showForm.value = true
}
},
{immediate: true},
modelValue,
(value) => {
console.log({value})
const p = secondsToPeriod(value?.relativePeriod)
period.value.durationUnit = p.unit
period.value.duration = p.amount
period.value.relativeTo = value?.relativeTo
},
{immediate: true},
)
function updateData() {
changed.value = true
if (reminder.value) {
reminder.value.relativePeriod = periodInput.sign * periodToSeconds(periodInput.duration.days, periodInput.duration.hours, periodInput.duration.minutes, 0)
reminder.value.relativeTo = periodInput.relativeTo
reminder.value.reminder = null
}
reminder.value.relativePeriod = period.value.sign * periodToSeconds(period.value.duration, period.value.durationUnit)
reminder.value.relativeTo = period.value.relativeTo
reminder.value.reminder = null
emit('update:modelValue', reminder.value)
}
function submitForm() {
updateData()
close()
}
const changed = ref(false)
function close() {
setTimeout(() => {
showForm.value = false
if (changed.value) {
changed.value = false
}
}, 200)
}
function formatDuration(reminderPeriod: number): string {
if (Math.abs(reminderPeriod) < 60) {
return '00:00'
}
const duration = secondsToPeriod(Math.abs(reminderPeriod))
return (duration.days > 0 ? `${duration.days} ${t('task.reminder.days')} `: '') +
('' + duration.hours).padStart(2, '0') + ':' +
('' + duration.minutes).padStart(2, '0')
}
const relativeToOptions = {
[REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE]: t('task.attributes.dueDate'),
[REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE]: t('task.attributes.startDate'),
[REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE]: t('task.attributes.endDate'),
} as const
const relativeTo = computed(() => relativeToOptions[periodInput.relativeTo])
function formatRelativeTo(relativeTo: IReminderPeriodRelativeTo | null): string | null {
switch (relativeTo) {
case REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE:
return t('task.attributes.dueDate')
case REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE:
return t('task.attributes.startDate')
case REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE:
return t('task.attributes.endDate')
default:
return relativeTo
}
}
</script>
<style lang="scss" scoped>
.input {
max-width: 5rem;
width: 4rem;
}
.reminder-period {
display: flex;
flex-direction: column;
gap: .25rem;
.close-button {
margin: 0.5rem;
width: calc(100% - 1rem);
.input, .select select {
width: 100% !important;
height: auto;
}
}
</style>

View File

@ -7,11 +7,14 @@
class="reminder-input"
>
<div class="reminder-detail">
<ReminderDetail :disabled="disabled" v-model="reminders[index]" />
<ReminderDetail
:disabled="disabled"
v-model="reminders[index]"
@update:model-value="updateData"/>
</div>
<div>
<BaseButton v-if="!disabled" @click="removeReminderByIndex(index)" class="remove">
<icon icon="times" />
<BaseButton v-if="!disabled" @click="removeReminderByIndex(index)" class="remove">
<icon icon="times"/>
</BaseButton>
</div>
</div>
@ -31,7 +34,7 @@
<script setup lang="ts">
import {reactive, ref, watch, type PropType} from 'vue'
import type { ITaskReminder } from '@/modelTypes/ITaskReminder'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import BaseButton from '@/components/base/BaseButton.vue'
import ReminderDetail from '@/components/tasks/partials/reminder-detail.vue'
@ -60,6 +63,7 @@ watch(
)
const isAddReminder = ref(false)
function toggleAddReminder() {
isAddReminder.value = !isAddReminder.value
}
@ -76,8 +80,9 @@ function editReminder(index: number) {
updateData()
}
function addNewReminder(newReminder : ITaskReminder) {
if (newReminder == null) {
function addNewReminder(newReminder: ITaskReminder) {
console.log('add new reminder', newReminder)
if (newReminder === null) {
return
}
reminders.value.push(newReminder)
@ -104,9 +109,11 @@ function removeReminderByIndex(index: number) {
margin-bottom: 0.75rem;
}
}
.reminder-detail {
width: 100%;
}
.remove {
color: var(--danger);
vertical-align: top;

View File

@ -1,20 +1,50 @@
import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_MINUTE} from '@/constants/date'
import {
SECONDS_A_DAY,
SECONDS_A_HOUR,
SECONDS_A_MINUTE,
SECONDS_A_MONTH,
SECONDS_A_WEEK,
SECONDS_A_YEAR,
} from '@/constants/date'
export type PeriodUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years'
/**
* Convert time period given as seconds to days, hour, minutes, seconds
*/
export function secondsToPeriod(seconds: number): {days: number, hours: number, minutes: number, seconds: number} {
export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: number } {
if (seconds % SECONDS_A_DAY === 0) {
if (seconds % SECONDS_A_WEEK === 0) {
return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK}
} else if (seconds % SECONDS_A_MONTH === 0) {
return {unit: 'months', amount: seconds / SECONDS_A_MONTH}
} else if (seconds % SECONDS_A_YEAR === 0) {
return {unit: 'years', amount: seconds / SECONDS_A_YEAR}
} else {
return {unit: 'days', amount: seconds / SECONDS_A_DAY}
}
}
return {
days: Math.floor(seconds / SECONDS_A_DAY),
hours: Math.floor(seconds % SECONDS_A_DAY / 3600),
minutes: Math.floor(seconds % SECONDS_A_HOUR / 60),
seconds: Math.floor(seconds % 60),
unit: 'hours',
amount: seconds / SECONDS_A_HOUR,
}
}
/**
* Convert time period of days, hour, minutes, seconds to duration in seconds
*/
export function periodToSeconds(days: number, hours: number, minutes: number, seconds: number): number {
return days * SECONDS_A_DAY + hours * SECONDS_A_HOUR + minutes * SECONDS_A_MINUTE + seconds
export function periodToSeconds(period: number, unit: PeriodUnit): number {
switch (unit) {
case 'minutes':
return period * SECONDS_A_MINUTE
case 'hours':
return period * SECONDS_A_HOUR
case 'days':
return period * SECONDS_A_DAY
case 'weeks':
return period * SECONDS_A_WEEK
}
return 0
}

View File

@ -22,6 +22,7 @@ import AttachmentModel from './attachment'
import SubscriptionModel from './subscription'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import TaskReminderModel from '@/models/taskReminder'
import {periodToSeconds, secondsToPeriod} from '@/helpers/time/period'
export const TASK_DEFAULT_COLOR = '#1973ff'
@ -37,21 +38,13 @@ export function getHexColor(hexColor: string): string {
* Parses `repeatAfterSeconds` into a usable js object.
*/
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
}
const period = secondsToPeriod(repeatAfterSeconds)
return {
type: period.unit,
amount: period.amount,
}
return repeatAfter
}
export default class TaskModel extends AbstractModel<ITask> implements ITask {

View File

@ -1,4 +1,6 @@
export const REPEAT_TYPES = {
Seconds: 'seconds',
Minutes: 'minutes',
Hours: 'hours',
Days: 'days',
Weeks: 'weeks',