dragging task in the calendar works visually

This commit is contained in:
2025-07-06 11:52:00 +02:00
parent 22ca3e9645
commit 0297fab83b
7 changed files with 94 additions and 17 deletions

View File

@@ -6,6 +6,7 @@ import type { DateTime } from 'luxon';
const events = defineModel<Event[]>('events', { required: true }) const events = defineModel<Event[]>('events', { required: true })
const date = defineModel<DateTime>('date', { required: true }) const date = defineModel<DateTime>('date', { required: true })
const draggedTask = defineModel<DraggedTask | undefined>('draggedTask', { required: true })
const emits = defineEmits<{ const emits = defineEmits<{
(e: 'createEvent', event: Event): void (e: 'createEvent', event: Event): void
@@ -15,7 +16,9 @@ const emits = defineEmits<{
<template> <template>
<UCard class="flex grow" :ui="{ body: 'w-full h-full' }"> <UCard class="flex grow" :ui="{ body: 'w-full h-full' }">
<Calendar @create="(event) => emits('createEvent', event)"v-model:events="events" v-model:date="date"></Calendar> <Calendar @create="(event) => emits('createEvent', event)" v-model:events="events" v-model:date="date" ,
v-model:dragged-task="draggedTask">
</Calendar>
</UCard> </UCard>
</template> </template>

View File

@@ -7,6 +7,7 @@ import { DateTime } from 'luxon';
const colorMode = useColorMode(); const colorMode = useColorMode();
const toast = useToast() const toast = useToast()
const instance = getCurrentInstance()
const currentTheme = ref<'dark' | 'system' | 'light'>(colorMode.preference as 'dark' | 'system' | 'light'); const currentTheme = ref<'dark' | 'system' | 'light'>(colorMode.preference as 'dark' | 'system' | 'light');
const showTaskCreateModal = ref(false); const showTaskCreateModal = ref(false);
@@ -20,6 +21,7 @@ const emits = defineEmits<{
(e: 'createTask', task: Task): void (e: 'createTask', task: Task): void
(e: 'deleteTask', id: number): void (e: 'deleteTask', id: number): void
(e: 'editTask', task: Task): void (e: 'editTask', task: Task): void
(e: 'scheduleTask', task: Task): void
}>() }>()
const isLight = computed(() => currentTheme.value === 'light'); const isLight = computed(() => currentTheme.value === 'light');
@@ -96,6 +98,7 @@ function deleteTask(task: Task) {
emits('deleteTask', task.id) emits('deleteTask', task.id)
} }
function editTask(task: Task) { function editTask(task: Task) {
emits('editTask', task) emits('editTask', task)
} }
@@ -110,6 +113,10 @@ function openTaskEditModal(task: Task) {
showTaskEditModal.value = true showTaskEditModal.value = true
} }
function scheduleTask(task: Task) {
emits('scheduleTask', task)
}
</script> </script>
<template> <template>
@@ -129,7 +136,8 @@ function openTaskEditModal(task: Task) {
<Title1>Tasks</Title1> <Title1>Tasks</Title1>
<div class="flex gap-2 flex-col"> <div class="flex gap-2 flex-col">
<ListItem v-for="task in todoTasks"> <ListItem v-for="task in todoTasks">
<div class="flex w-full gap-4 items-center"> <div class="flex w-full gap-4 items-center" @dragstart="scheduleTask(task)"
draggable="true">
<span <span
class="grow overflow-scroll py-3 overflow-shadow flex flex-row gap-2 items-center"> class="grow overflow-scroll py-3 overflow-shadow flex flex-row gap-2 items-center">
<UCheckbox v-model="task.done" @change="() => editTask(task)" />{{ task.title }} <UCheckbox v-model="task.done" @change="() => editTask(task)" />{{ task.title }}
@@ -144,7 +152,8 @@ function openTaskEditModal(task: Task) {
</ListItem> </ListItem>
<USeparator label="Done" v-if="todoTasks.length !== 0" /> <USeparator label="Done" v-if="todoTasks.length !== 0" />
<ListItem v-for="task in doneTasks"> <ListItem v-for="task in doneTasks">
<div class="flex w-full gap-4 items-center"> <div class="flex w-full gap-4 items-center" @dragstart="scheduleTask(task)"
draggable="true">
<span <span
class="grow overflow-scroll py-3 overflow-shadow flex flex-row gap-2 items-center"> class="grow overflow-scroll py-3 overflow-shadow flex flex-row gap-2 items-center">
<UCheckbox v-model="task.done" @change="() => editTask(task)" />{{ task.title }} <UCheckbox v-model="task.done" @change="() => editTask(task)" />{{ task.title }}

View File

@@ -5,7 +5,7 @@ import * as z from 'zod';
const open = defineModel<boolean>('open', { required: true }) const open = defineModel<boolean>('open', { required: true })
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'submnitted', event: SimpleTask): void (e: 'submnitted', event: Task): void
(e: 'canceled'): void (e: 'canceled'): void
}>() }>()
@@ -63,14 +63,15 @@ function submit() {
return return
} }
emit('submnitted', { emit('submnitted', Task.fromSimpleTask({
id: props.input.id, id: props.input.id,
title: form.data.title, title: form.data.title,
done: props.input.done ?? false, done: props.input.done ?? false,
description: form.data.description, description: form.data.description,
estimated_time: form.data.estimated_time, estimated_time: form.data.estimated_time,
due_date: DateTime.fromISO(form.data.due_date) due_date: DateTime.fromISO(form.data.due_date),
}) scheduled_at: props.input.scheduled_at
}))
open.value = false open.value = false
} }

View File

@@ -8,6 +8,7 @@ import EventFormModal from '../EventFormModal.vue';
const events = defineModel<Event[]>('events', { required: true }) const events = defineModel<Event[]>('events', { required: true })
const date = defineModel<DateTime>('date', { required: true }) const date = defineModel<DateTime>('date', { required: true })
const draggedTask = defineModel<DraggedTask | undefined>('draggedTask', { required: true })
const draggedEvent = ref<DraggedEvent | undefined>() const draggedEvent = ref<DraggedEvent | undefined>()
const createInput = ref<Partial<SimpleEvent>>({}) const createInput = ref<Partial<SimpleEvent>>({})
const createModalOpened = ref(false) const createModalOpened = ref(false)
@@ -161,7 +162,8 @@ function moveEvent(event: Event) {
<EventFormModal action="edit" @submnitted="event => edit(event)" :input="editInput" <EventFormModal action="edit" @submnitted="event => edit(event)" :input="editInput"
v-model:open="editModalOpened" /> v-model:open="editModalOpened" />
<UModal v-model:open="deleteModalOpened" title="Delete Event" description="Are you sure you want to delete this event?"> <UModal v-model:open="deleteModalOpened" title="Delete Event"
description="Are you sure you want to delete this event?">
<template #footer> <template #footer>
<UButton variant="solid" @click="deleteEvent"> <UButton variant="solid" @click="deleteEvent">
Delete Delete
@@ -176,8 +178,8 @@ function moveEvent(event: Event) {
<CalendarHeader :seperators="seperators" /> <CalendarHeader :seperators="seperators" />
<CalendarCollumn v-for="day in days" :seperators="seperators" :day="day.date" :events="day.events" <CalendarCollumn v-for="day in days" :seperators="seperators" :day="day.date" :events="day.events"
:date="date" v-model:draggedEvent="draggedEvent" @quick-create="openCreateModal" :date="date" v-model:draggedEvent="draggedEvent" @quick-create="openCreateModal" @edit="openEditModal"
@edit="openEditModal" @delete="openDeleteModal" @moved="moveEvent" /> @delete="openDeleteModal" @moved="moveEvent" v-model:dragged-task="draggedTask" />
</div> </div>
</div> </div>

View File

@@ -25,6 +25,7 @@ const startY = ref(0)
const endY = ref(0) const endY = ref(0)
const column = useTemplateRef('column') const column = useTemplateRef('column')
const draggedEvent = defineModel<DraggedEvent | undefined>('draggedEvent') const draggedEvent = defineModel<DraggedEvent | undefined>('draggedEvent')
const draggedTask = defineModel<DraggedTask | undefined>('draggedTask')
const height = computed(() => { const height = computed(() => {
return Math.abs(endY.value - startY.value) return Math.abs(endY.value - startY.value)
@@ -83,6 +84,29 @@ function eventMove(mouseEvent: MouseEvent, event: Event) {
} }
function dragover(e: DragEvent) { function dragover(e: DragEvent) {
drawDraggedEvent(e)
drawDraggedTask(e)
}
function drawDraggedTask(event: DragEvent) {
if (draggedTask.value === undefined) {
return;
}
if (draggedTask.value.dragInfo === undefined) {
draggedTask.value.dragInfo = {
height: (draggedTask.value.target.estimated_time / 60 / 24) * (column.value?.offsetHeight ?? 0),
top: absoluteToRelativeY(event.clientY),
date: props.day
}
drawDraggedTask(event)
}
draggedTask.value.dragInfo.top = absoluteToRelativeY(event.clientY)
draggedTask.value.dragInfo.date = props.day
}
function drawDraggedEvent(event: DragEvent) {
if (draggedEvent.value === undefined) { if (draggedEvent.value === undefined) {
return return
} }
@@ -92,7 +116,7 @@ function dragover(e: DragEvent) {
} }
draggedEvent.value.top = absoluteToRelativeY(e.clientY) - draggedEvent.value.offset draggedEvent.value.top = absoluteToRelativeY(event.clientY) - draggedEvent.value.offset
} }
function dragDrop(_: DragEvent) { function dragDrop(_: DragEvent) {
@@ -134,6 +158,9 @@ function dragDrop(_: DragEvent) {
<div v-if="draggedEvent !== undefined && draggedEvent.date.equals(props.day)" <div v-if="draggedEvent !== undefined && draggedEvent.date.equals(props.day)"
class="absolute w-11/12 top-20 bg-black opacity-45 rounded-lg" class="absolute w-11/12 top-20 bg-black opacity-45 rounded-lg"
:style="{ height: `${draggedEvent.height}px`, top: `${draggedEvent.top}px` }"></div> :style="{ height: `${draggedEvent.height}px`, top: `${draggedEvent.top}px` }"></div>
<div v-if="draggedTask !== undefined && draggedTask.dragInfo !== undefined && draggedTask.dragInfo.date.equals(props.day)"
class="absolute w-11/12 top-20 bg-black opacity-45 rounded-lg"
:style="{ height: `${draggedTask.dragInfo.height}px`, top: `${draggedTask.dragInfo.top}px` }"></div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -8,6 +8,7 @@ import { Event, type SerializableEvent } from '~/utils/event';
const date = ref<DateTime>(DateTime.now()) const date = ref<DateTime>(DateTime.now())
const events = ref<Event[]>([]) const events = ref<Event[]>([])
const tasks = ref<Task[]>([]) const tasks = ref<Task[]>([])
const draggedTask = ref<DraggedTask | undefined>(undefined)
const { data: eventsResponse } = await useAsyncData<SerializableEvent[]>( const { data: eventsResponse } = await useAsyncData<SerializableEvent[]>(
'events', 'events',
@@ -42,13 +43,18 @@ async function deleteTask(id: number) {
await refresh() await refresh()
} }
function scheduleTask(task: Task) {
draggedTask.value = { target: task, dragInfo: undefined }
}
</script> </script>
<template> <template>
<div class="h-screen w-screen p-4 flex flex-row gap-5"> <div class="h-screen w-screen p-4 flex flex-row gap-5">
<Sidebar v-if="tasks !== null" v-model:tasks="tasks" v-model:date="date" @create-task="postTask" <Sidebar v-if="tasks !== null" v-model:tasks="tasks" v-model:date="date" @create-task="postTask"
@delete-task="deleteTask" /> @delete-task="deleteTask" @schedule-task="scheduleTask"/>
<MainContent v-if="events !== null" v-model:events="events" v-model:date="date" @create-event="postEvent" /> <MainContent v-if="events !== null" v-model:events="events" v-model:date="date"
v-model:dragged-task="draggedTask" @create-event="postEvent" />
</div> </div>
</template> </template>

View File

@@ -7,7 +7,8 @@ export class Task {
public description: string, public description: string,
public done: boolean, public done: boolean,
public estimated_time: number, public estimated_time: number,
public due_date: DateTime | undefined public due_date: DateTime | undefined,
public scheduled_at: DateTime | undefined
) { } ) { }
static fromSimpleTask(simpleTask: SimpleTask) { static fromSimpleTask(simpleTask: SimpleTask) {
@@ -17,7 +18,8 @@ export class Task {
simpleTask.description, simpleTask.description,
simpleTask.done, simpleTask.done,
simpleTask.estimated_time, simpleTask.estimated_time,
simpleTask.due_date simpleTask.due_date,
simpleTask.scheduled_at
) )
} }
@@ -29,9 +31,18 @@ export class Task {
serializableTask.description, serializableTask.description,
serializableTask.done, serializableTask.done,
serializableTask.estimated_time, serializableTask.estimated_time,
DateTime.now() stringToDate(serializableTask.due_date),
stringToDate(serializableTask.scheduled_at),
) )
} }
isPersistent() {
return this.id !== undefined
}
isScheduled() {
return this.scheduled_at !== undefined
}
} }
export type SimpleTask = { export type SimpleTask = {
@@ -40,6 +51,7 @@ export type SimpleTask = {
description: string description: string
done: boolean done: boolean
estimated_time: number estimated_time: number
scheduled_at: DateTime | undefined
due_date: DateTime | undefined due_date: DateTime | undefined
} }
@@ -50,7 +62,24 @@ export type SerializableTask = {
done: boolean done: boolean
estimated_time: number estimated_time: number
due_date: string | undefined due_date: string | undefined
scheduled_at: string | undefined
created_at: string created_at: string
updated_at: string updated_at: string
userid: string userid: string
} }
export type DraggedTask = {
target: Task,
dragInfo: {
top: number,
date: DateTime
height: number
} | undefined
}
function stringToDate(date: string | undefined) {
if (date === undefined) {
return undefined
}
return DateTime.fromISO(date)
}