diff --git a/packages/core/src/__tests__/helpers/task.factory.ts b/packages/core/src/__tests__/helpers/task.factory.ts new file mode 100644 index 000000000..fbf9efa7c --- /dev/null +++ b/packages/core/src/__tests__/helpers/task.factory.ts @@ -0,0 +1,18 @@ +import { faker } from "@faker-js/faker"; +import { Task } from "@web/common/types/task.types"; + +/** + * Creates a mock task with optional overrides + * @param overrides - Partial task properties to override defaults + * @returns A complete Task object + */ +export function createMockTask(overrides: Partial = {}): Task { + return { + id: faker.string.uuid(), + title: faker.lorem.sentence({ min: 3, max: 5 }), + status: "todo", + order: faker.number.int({ min: 0, max: 100 }), + createdAt: faker.date.recent().toISOString(), + ...overrides, + }; +} diff --git a/packages/web/src/common/constants/web.constants.ts b/packages/web/src/common/constants/web.constants.ts index a682df4ea..4d1c93c7e 100644 --- a/packages/web/src/common/constants/web.constants.ts +++ b/packages/web/src/common/constants/web.constants.ts @@ -11,6 +11,7 @@ export const ID_GRID_EVENTS_ALLDAY = "allDayEvents"; export const ID_GRID_EVENTS_TIMED = "timedEvents"; export const ID_SOMEDAY_WEEK_COLUMN = "somedayWeekColumn"; export const ID_GRID_MAIN = "mainGrid"; +export const ID_DROPPABLE_TASKS = "task-list"; export const ID_REMINDER_INPUT = "reminderInput"; export const ID_MAIN = "mainSection"; export const ID_DATEPICKER_SIDEBAR = "sidebarDatePicker"; diff --git a/packages/web/src/common/hooks/useEventDNDActions.ts b/packages/web/src/common/hooks/useEventDNDActions.ts index 37b5116d9..8368e9e37 100644 --- a/packages/web/src/common/hooks/useEventDNDActions.ts +++ b/packages/web/src/common/hooks/useEventDNDActions.ts @@ -2,6 +2,7 @@ import { useCallback } from "react"; import { Active, DragEndEvent, Over, useDndMonitor } from "@dnd-kit/core"; import { Categories_Event } from "@core/types/event.types"; import dayjs from "@core/util/date/dayjs"; +import { getUserId } from "@web/auth/auth.util"; import { ID_GRID_ALLDAY_ROW, ID_GRID_MAIN, @@ -12,12 +13,18 @@ import { setFloatingReferenceAtCursor, } from "@web/common/hooks/useOpenAtCursor"; import { useUpdateEvent } from "@web/common/hooks/useUpdateEvent"; +import { Task } from "@web/common/types/task.types"; import { Schema_GridEvent } from "@web/common/types/web.event.types"; import { reorderGrid } from "@web/common/utils/dom/grid-organization.util"; import { getCalendarEventElementFromGrid } from "@web/common/utils/event/event.util"; import { selectEventById } from "@web/ducks/events/selectors/event.selectors"; +import { createEventSlice } from "@web/ducks/events/slices/event.slice"; +import { pendingEventsSlice } from "@web/ducks/events/slices/pending.slice"; import { store } from "@web/store"; +import { useAppDispatch } from "@web/store/store.hooks"; +import { useDateInView } from "@web/views/Day/hooks/navigation/useDateInView"; import { getSnappedMinutes } from "@web/views/Day/util/agenda/agenda.util"; +import { handleTaskToEventConversion } from "@web/views/Day/util/task/task.util"; const shouldSaveImmediately = (_id: string) => { const storeEvent = selectEventById(store.getState(), _id); @@ -36,6 +43,47 @@ const setReference = (_id: string) => { export function useEventDNDActions() { const updateEvent = useUpdateEvent(); + const dispatch = useAppDispatch(); + const dateInView = useDateInView(); + + const convertTaskToEventOnAgenda = useCallback( + async ( + task: Task, + active: Active, + over: Over, + deleteTask: () => void, + isAllDay: boolean = false, + ) => { + const userId = await getUserId(); + if (!userId) return; + + const event = handleTaskToEventConversion( + task, + active, + over, + dateInView, + userId, + isAllDay, + ); + + if (!event) return; + + // Add event to pending events and create optimistically + dispatch(pendingEventsSlice.actions.add(event._id!)); + dispatch(createEventSlice.actions.request(event)); + + // Note: Task deletion happens immediately for simplicity. The event saga + // will remove the optimistic event if creation fails, but the task cannot + // be restored because tasks are not in Redux (only local state + localStorage). + // Proper error handling would require: (1) moving tasks to Redux, (2) storing + // task-to-event mappings, and (3) dispatching deleteTask actions from the saga. + deleteTask(); + + reorderGrid(); + setReference(event._id!); + }, + [dispatch, dateInView], + ); const moveTimedAroundMainGridDayView = useCallback( (event: Schema_GridEvent, active: Active, over: Over) => { @@ -126,25 +174,37 @@ export function useEventDNDActions() { (e: DragEndEvent) => { const { active, over } = e; const { data } = active; - const { view, type, event } = data.current ?? {}; + const { view, type, event, task, deleteTask } = data.current ?? {}; - if (!over?.id || !event) return; + if (!over?.id) return; const switchCase = `${view}-${type}-to-${over.id}`; switch (switchCase) { + case `day-task-to-${ID_GRID_MAIN}`: + if (!task || !deleteTask) break; + convertTaskToEventOnAgenda(task, active, over, deleteTask, false); + break; + case `day-task-to-${ID_GRID_ALLDAY_ROW}`: + if (!task || !deleteTask) break; + convertTaskToEventOnAgenda(task, active, over, deleteTask, true); + break; case `day-${Categories_Event.ALLDAY}-to-${ID_GRID_MAIN}`: + if (!event) break; moveAllDayToMainGridDayView(event, active, over); break; case `day-${Categories_Event.TIMED}-to-${ID_GRID_MAIN}`: + if (!event) break; moveTimedAroundMainGridDayView(event, active, over); break; case `day-${Categories_Event.TIMED}-to-${ID_GRID_ALLDAY_ROW}`: + if (!event) break; moveTimedToAllDayGridDayView(event); break; } }, [ + convertTaskToEventOnAgenda, moveAllDayToMainGridDayView, moveTimedAroundMainGridDayView, moveTimedToAllDayGridDayView, diff --git a/packages/web/src/components/DND/Draggable.tsx b/packages/web/src/components/DND/Draggable.tsx index 97dab9599..3675d9e96 100644 --- a/packages/web/src/components/DND/Draggable.tsx +++ b/packages/web/src/components/DND/Draggable.tsx @@ -17,12 +17,17 @@ import { import { CSS } from "@dnd-kit/utilities"; import { useMergeRefs } from "@floating-ui/react"; import { Categories_Event } from "@core/types/event.types"; +import { Task } from "@web/common/types/task.types"; import { Schema_GridEvent } from "@web/common/types/web.event.types"; +export type DraggableDataType = Categories_Event | "task"; + export interface DraggableDNDData { - type: Categories_Event; + type: DraggableDataType; event: Schema_GridEvent | null; + task: Task | null; view: "day" | "week" | "now"; + deleteTask: (() => void) | null; } export interface DNDChildProps diff --git a/packages/web/src/views/Day/components/Agenda/Events/AllDayAgendaEvent/DraggableAllDayAgendaEvent.tsx b/packages/web/src/views/Day/components/Agenda/Events/AllDayAgendaEvent/DraggableAllDayAgendaEvent.tsx index 865253681..1a38391d4 100644 --- a/packages/web/src/views/Day/components/Agenda/Events/AllDayAgendaEvent/DraggableAllDayAgendaEvent.tsx +++ b/packages/web/src/views/Day/components/Agenda/Events/AllDayAgendaEvent/DraggableAllDayAgendaEvent.tsx @@ -53,7 +53,9 @@ export const DraggableAllDayAgendaEvent = memo( data: { event, type: Categories_Event.ALLDAY, + task: null, view: "day", + deleteTask: null, }, disabled: isDisabled, }} diff --git a/packages/web/src/views/Day/components/Agenda/Events/TimedAgendaEvent/DraggableTimedAgendaEvent.tsx b/packages/web/src/views/Day/components/Agenda/Events/TimedAgendaEvent/DraggableTimedAgendaEvent.tsx index d6c2b81ec..589c0449f 100644 --- a/packages/web/src/views/Day/components/Agenda/Events/TimedAgendaEvent/DraggableTimedAgendaEvent.tsx +++ b/packages/web/src/views/Day/components/Agenda/Events/TimedAgendaEvent/DraggableTimedAgendaEvent.tsx @@ -92,7 +92,9 @@ export const DraggableTimedAgendaEvent = memo( data: { event: event, type: Categories_Event.TIMED, + task: null, view: "day", + deleteTask: null, }, disabled: isDisabled, }} diff --git a/packages/web/src/views/Day/components/Task/DraggableTask.test.tsx b/packages/web/src/views/Day/components/Task/DraggableTask.test.tsx index cd9c11395..8ef6ead9d 100644 --- a/packages/web/src/views/Day/components/Task/DraggableTask.test.tsx +++ b/packages/web/src/views/Day/components/Task/DraggableTask.test.tsx @@ -1,5 +1,4 @@ import React, { act } from "react"; -import { DragDropContext, Droppable } from "@hello-pangea/dnd"; import { fireEvent, screen } from "@testing-library/react"; import { render } from "@web/__tests__/__mocks__/mock.render"; import { Task } from "@web/common/types/task.types"; @@ -52,22 +51,7 @@ const renderDraggableTask = ( tasksProps = defaultTasksProps, ) => { return act(() => - render( - - - {(provided) => ( -
- - {provided.placeholder} -
- )} -
-
, - ), + render(), ); }; diff --git a/packages/web/src/views/Day/components/Task/DraggableTask.tsx b/packages/web/src/views/Day/components/Task/DraggableTask.tsx index 2a3e53b61..e5bc0b84d 100644 --- a/packages/web/src/views/Day/components/Task/DraggableTask.tsx +++ b/packages/web/src/views/Day/components/Task/DraggableTask.tsx @@ -1,10 +1,12 @@ import classNames from "classnames"; +import { useDraggable } from "@dnd-kit/core"; +import { CSS } from "@dnd-kit/utilities"; import { autoUpdate, inline, offset, useFloating } from "@floating-ui/react"; -import { Draggable } from "@hello-pangea/dnd"; import { DotsSixVerticalIcon } from "@phosphor-icons/react"; -import { Task as ITask } from "@web/common/types/task.types"; -import { getStyle } from "@web/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEvent/styled"; -import { Task } from "@web/views/Day/components/Task/Task"; +import { Task } from "@web/common/types/task.types"; +import { DNDChildProps, Draggable } from "@web/components/DND/Draggable"; +import { DraggableTaskHandle } from "@web/views/Day/components/Task/DraggableTaskHandle"; +import { Task as TaskComponent } from "@web/views/Day/components/Task/Task"; import { useTasks } from "@web/views/Day/hooks/tasks/useTasks"; export function DraggableTask({ @@ -12,7 +14,7 @@ export function DraggableTask({ index, tasksProps, }: { - task: ITask; + task: Task; index: number; tasksProps: ReturnType; }) { @@ -38,79 +40,124 @@ export function DraggableTask({ onTitleChange, onStatusToggle, migrateTask, + deleteTask, } = tasksProps; return ( deleteTask(task.id), + }, + disabled: tasks.length === 1, + }} + as="div" + id={task.id} + className={`group relative mr-2 select-none`} + ref={(e) => { + refs.setReference(e); + update(); + }} + asChild > - {(draggableProvider, draggableSnapshot) => ( -
{ - draggableProvider.innerRef(e); - refs.setReference(e); - update(); - }} - > - {tasks.length > 1 ? ( - - ) : null} + + + ); +} + +function DraggableTaskInner({ + task, + index, + tasks, + editingTaskId, + editingTitle, + setSelectedTaskIndex, + onCheckboxKeyDown, + onInputBlur, + onInputClick, + onInputKeyDown, + onTitleChange, + onStatusToggle, + migrateTask, + refs, + floatingStyles, + dndProps, +}: { + task: Task; + index: number; + tasks: Task[]; + editingTaskId: string | null; + editingTitle: string; + setSelectedTaskIndex: (index: number) => void; + onCheckboxKeyDown: ( + e: React.KeyboardEvent, + taskId: string, + title: string, + ) => void; + onInputBlur: (taskId: string) => void; + onInputClick: (taskId: string) => void; + onInputKeyDown: (e: React.KeyboardEvent, taskId: string) => void; + onTitleChange: (title: string) => void; + onStatusToggle: (id: string) => void; + migrateTask: (id: string, direction: "forward" | "backward") => void; + refs: ReturnType["refs"]; + floatingStyles: React.CSSProperties; + dndProps?: DNDChildProps; +}) { + const isDragging = dndProps?.isDragging ?? false; - + return ( +
+ -
- Press space to start dragging this task. -
-
- )} - + + +
+ Press space to start dragging this task. +
+
); } diff --git a/packages/web/src/views/Day/components/Task/DraggableTaskHandle.tsx b/packages/web/src/views/Day/components/Task/DraggableTaskHandle.tsx new file mode 100644 index 000000000..a20d2cace --- /dev/null +++ b/packages/web/src/views/Day/components/Task/DraggableTaskHandle.tsx @@ -0,0 +1,54 @@ +import classNames from "classnames"; +import { UseFloatingReturn } from "@floating-ui/react"; +import { DotsSixVerticalIcon } from "@phosphor-icons/react"; +import { Task } from "@web/common/types/task.types"; +import { DNDChildProps } from "@web/components/DND/Draggable"; + +interface DraggableTaskHandleProps { + task: Task; + index: number; + tasksLength: number; + isDragging: boolean; + listeners: DNDChildProps["listeners"]; + refs: UseFloatingReturn["refs"]; + floatingStyles: React.CSSProperties; + onFocus: (index: number) => void; +} + +export function DraggableTaskHandle({ + task, + index, + tasksLength, + isDragging, + listeners, + refs, + floatingStyles, + onFocus, +}: DraggableTaskHandleProps) { + if (tasksLength <= 1) return null; + + return ( + + ); +} diff --git a/packages/web/src/views/Day/components/TaskList/TaskList.tsx b/packages/web/src/views/Day/components/TaskList/TaskList.tsx index 6742cded1..87b6123c3 100644 --- a/packages/web/src/views/Day/components/TaskList/TaskList.tsx +++ b/packages/web/src/views/Day/components/TaskList/TaskList.tsx @@ -5,7 +5,6 @@ import { TaskContextMenuWrapper } from "@web/views/Day/components/ContextMenu/Ta import { TaskListHeader } from "@web/views/Day/components/TaskList/TaskListHeader"; import { Tasks } from "@web/views/Day/components/Tasks/Tasks"; import { useTaskListInputFocus } from "@web/views/Day/components/Tasks/useTaskListInputFocus"; -import { DNDTasksProvider } from "@web/views/Day/context/DNDTasksContext"; import { useTasks } from "@web/views/Day/hooks/tasks/useTasks"; export function TaskList() { @@ -64,9 +63,7 @@ export function TaskList() { className="flex flex-1 flex-col gap-2 overflow-hidden p-4" > - - - +
diff --git a/packages/web/src/views/Day/components/Tasks/Tasks.tsx b/packages/web/src/views/Day/components/Tasks/Tasks.tsx index 4be35efbf..de3ccc7bc 100644 --- a/packages/web/src/views/Day/components/Tasks/Tasks.tsx +++ b/packages/web/src/views/Day/components/Tasks/Tasks.tsx @@ -1,45 +1,30 @@ -import { DragDropContext, Droppable } from "@hello-pangea/dnd"; +import { ID_DROPPABLE_TASKS } from "@web/common/constants/web.constants"; +import { Droppable } from "@web/components/DND/Droppable"; import { DropZone } from "@web/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventsContainer/Dropzone"; import { DraggableTask } from "@web/views/Day/components/Task/DraggableTask"; -import { useDNDTasksContext } from "@web/views/Day/hooks/tasks/useDNDTasks"; import { useTasks } from "@web/views/Day/hooks/tasks/useTasks"; export const Tasks = () => { const tasksProps = useTasks(); - const { onDragStart, onDragUpdate, onDragEnd } = useDNDTasksContext(); return ( - - - {(droppableProvider, droppableSnapshot) => ( - - {tasksProps.tasks.map((task, index) => ( - - ))} - - {droppableProvider.placeholder} - - )} - - + {tasksProps.tasks.map((task, index) => ( + + ))} + ); }; diff --git a/packages/web/src/views/Day/util/task/convertTaskToEvent.test.ts b/packages/web/src/views/Day/util/task/convertTaskToEvent.test.ts new file mode 100644 index 000000000..2b6981bc5 --- /dev/null +++ b/packages/web/src/views/Day/util/task/convertTaskToEvent.test.ts @@ -0,0 +1,80 @@ +import { createMockTask } from "@core/__tests__/helpers/task.factory"; +import { Origin, Priorities } from "@core/constants/core.constants"; +import dayjs from "@core/util/date/dayjs"; +import { convertTaskToEvent } from "./convertTaskToEvent"; + +describe("convertTaskToEvent", () => { + const mockUserId = "user123"; + const mockTask = createMockTask({ + id: "task1", + title: "Test Task", + description: "Test description", + }); + + it("should convert a task to an event with default duration", () => { + const startTime = dayjs("2025-01-01T10:00:00.000Z"); + const event = convertTaskToEvent(mockTask, startTime, 30, mockUserId); + + expect(event).toMatchObject({ + title: "Test Task", + description: "Test description", + startDate: "2025-01-01T10:00:00.000Z", + endDate: "2025-01-01T10:30:00.000Z", + isAllDay: false, + isSomeday: false, + user: mockUserId, + priority: Priorities.UNASSIGNED, + origin: Origin.COMPASS, + }); + expect(event._id).toBeDefined(); + }); + + it("should convert a task to an event with custom duration", () => { + const startTime = dayjs("2025-01-01T14:30:00.000Z"); + const event = convertTaskToEvent(mockTask, startTime, 30, mockUserId); + + expect(event).toMatchObject({ + startDate: "2025-01-01T14:30:00.000Z", + endDate: "2025-01-01T15:00:00.000Z", + }); + }); + + it("should handle task without description", () => { + const taskWithoutDescription = createMockTask({ + description: undefined, + }); + const startTime = dayjs("2025-01-01T10:00:00.000Z"); + const event = convertTaskToEvent( + taskWithoutDescription, + startTime, + 30, + mockUserId, + ); + + expect(event.description).toBe(""); + }); + + it("should generate a unique ObjectId for each conversion", () => { + const startTime = dayjs("2025-01-01T10:00:00.000Z"); + const event1 = convertTaskToEvent(mockTask, startTime, 30, mockUserId); + const event2 = convertTaskToEvent(mockTask, startTime, 30, mockUserId); + + expect(event1._id).not.toBe(event2._id); + }); + + it("should create an all-day event when isAllDay is true", () => { + const startTime = dayjs("2025-01-01T00:00:00.000Z"); + const event = convertTaskToEvent(mockTask, startTime, 30, mockUserId, true); + + expect(event.isAllDay).toBe(true); + expect(event).toMatchObject({ + title: "Test Task", + description: "Test description", + startDate: "2025-01-01T00:00:00.000Z", + endDate: "2025-01-01T00:30:00.000Z", + user: mockUserId, + priority: Priorities.UNASSIGNED, + origin: Origin.COMPASS, + }); + }); +}); diff --git a/packages/web/src/views/Day/util/task/convertTaskToEvent.ts b/packages/web/src/views/Day/util/task/convertTaskToEvent.ts new file mode 100644 index 000000000..94586753c --- /dev/null +++ b/packages/web/src/views/Day/util/task/convertTaskToEvent.ts @@ -0,0 +1,28 @@ +import { ObjectId } from "bson"; +import { Origin, Priorities } from "@core/constants/core.constants"; +import { Schema_Event_Core } from "@core/types/event.types"; +import dayjs, { Dayjs } from "@core/util/date/dayjs"; +import { Task } from "@web/common/types/task.types"; + +export function convertTaskToEvent( + task: Task, + startTime: Dayjs, + durationMinutes: number = 30, + userId: string, + isAllDay: boolean = false, +): Schema_Event_Core { + const endTime = startTime.add(durationMinutes, "minute"); + + return { + _id: new ObjectId().toString(), + title: task.title, + description: task.description || "", + startDate: startTime.toISOString(), + endDate: endTime.toISOString(), + isAllDay, + isSomeday: false, + user: userId, + priority: Priorities.UNASSIGNED, + origin: Origin.COMPASS, + }; +} diff --git a/packages/web/src/views/Day/util/task/task.util.test.ts b/packages/web/src/views/Day/util/task/task.util.test.ts new file mode 100644 index 000000000..4a1415b19 --- /dev/null +++ b/packages/web/src/views/Day/util/task/task.util.test.ts @@ -0,0 +1,127 @@ +import { Active, Over } from "@dnd-kit/core"; +import { createMockTask } from "@core/__tests__/helpers/task.factory"; +import dayjs from "@core/util/date/dayjs"; +import * as agendaUtil from "@web/views/Day/util/agenda/agenda.util"; +import { handleTaskToEventConversion } from "./task.util"; + +jest.mock("@web/views/Day/util/agenda/agenda.util"); + +describe("handleTaskToEventConversion", () => { + const userId = "user123"; + const dateInView = dayjs("2024-01-15"); + const mockTask = createMockTask({ + title: "Test Task", + description: "Test Description", + }); + + const mockActive: Active = { + id: "task-1", + data: { + current: {}, + }, + rect: { + current: { + initial: null, + translated: null, + }, + }, + }; + + const mockOver: Over = { + id: "grid-main", + data: { + current: {}, + }, + rect: { + width: 0, + height: 0, + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + disabled: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should create a timed event when isAllDay is false", () => { + const snappedMinutes = 600; // 10:00 AM + (agendaUtil.getSnappedMinutes as jest.Mock).mockReturnValue(snappedMinutes); + + const result = handleTaskToEventConversion( + mockTask, + mockActive, + mockOver, + dateInView, + userId, + false, + ); + + expect(result).not.toBeNull(); + expect(result?.title).toBe("Test Task"); + expect(result?.description).toBe("Test Description"); + expect(result?.isAllDay).toBe(false); + expect(result?.user).toBe(userId); + + // Check that startDate is set to 10:00 AM on the dateInView + const startDate = dayjs(result?.startDate); + expect(startDate.format("YYYY-MM-DD HH:mm")).toBe("2024-01-15 10:00"); + + // Check that endDate is 30 minutes after startDate + const endDate = dayjs(result?.endDate); + expect(endDate.diff(startDate, "minute")).toBe(30); + }); + + it("should create an all-day event when isAllDay is true", () => { + const result = handleTaskToEventConversion( + mockTask, + mockActive, + mockOver, + dateInView, + userId, + true, + ); + + expect(result).not.toBeNull(); + expect(result?.title).toBe("Test Task"); + expect(result?.isAllDay).toBe(true); + expect(result?.user).toBe(userId); + + // Check that startDate is at the start of the day + const startDate = dayjs(result?.startDate); + expect(startDate.format("YYYY-MM-DD HH:mm")).toBe("2024-01-15 00:00"); + }); + + it("should return null when getSnappedMinutes returns null for timed events", () => { + (agendaUtil.getSnappedMinutes as jest.Mock).mockReturnValue(null); + + const result = handleTaskToEventConversion( + mockTask, + mockActive, + mockOver, + dateInView, + userId, + false, + ); + + expect(result).toBeNull(); + }); + + it("should not call getSnappedMinutes for all-day events", () => { + const getSnappedMinutesSpy = jest.spyOn(agendaUtil, "getSnappedMinutes"); + + handleTaskToEventConversion( + mockTask, + mockActive, + mockOver, + dateInView, + userId, + true, + ); + + expect(getSnappedMinutesSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/web/src/views/Day/util/task/task.util.ts b/packages/web/src/views/Day/util/task/task.util.ts new file mode 100644 index 000000000..8da92ee1b --- /dev/null +++ b/packages/web/src/views/Day/util/task/task.util.ts @@ -0,0 +1,32 @@ +import { Active, Over } from "@dnd-kit/core"; +import { Schema_Event_Core } from "@core/types/event.types"; +import dayjs from "@core/util/date/dayjs"; +import { Task } from "@web/common/types/task.types"; +import { getSnappedMinutes } from "@web/views/Day/util/agenda/agenda.util"; +import { convertTaskToEvent } from "@web/views/Day/util/task/convertTaskToEvent"; + +export function handleTaskToEventConversion( + task: Task, + active: Active, + over: Over, + dateInView: dayjs.Dayjs, + userId: string, + isAllDay: boolean, +): Schema_Event_Core | null { + let startTime: dayjs.Dayjs; + + if (isAllDay) { + // For all-day events, use the start of the day + startTime = dateInView.startOf("day"); + } else { + // For timed events, snap to grid + const snappedMinutes = getSnappedMinutes(active, over); + if (snappedMinutes === null) return null; + startTime = dateInView.startOf("day").add(snappedMinutes, "minute"); + } + + // Convert task to event (default duration is 30 minutes) + const event = convertTaskToEvent(task, startTime, 30, userId, isAllDay); + + return event; +}