diff --git a/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-timeline-header.vue b/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-timeline-header.vue index 64de15c..b11423e 100644 --- a/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-timeline-header.vue +++ b/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-timeline-header.vue @@ -42,6 +42,8 @@ const getHourPosition = (hour: number): number => { position: relative height: 30px width: calc(24 * 60px) + user-select: none + cursor: inherit @media (max-width: 768px) width: calc(24 * 50px) @@ -51,6 +53,7 @@ const getHourPosition = (hour: number): number => { top: 0 height: 30px border-left: 1px solid #e0e0e0 + cursor: inherit .hour-label position: absolute @@ -59,5 +62,6 @@ const getHourPosition = (hour: number): number => { font-size: 11px color: #666 white-space: nowrap + cursor: inherit diff --git a/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-timeline.vue b/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-timeline.vue index 423c6d7..ca69dc4 100644 --- a/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-timeline.vue +++ b/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-timeline.vue @@ -4,7 +4,13 @@ Načítavam...
-
+
+ + +
@@ -211,6 +222,7 @@ interface CollisionSegment { interface Props { isLoading: boolean; employee: EmployeeFullData; + date: Date | string | null; scheduledBlocks: TimelineTask[]; otherTimeblocks?: TimeblockData[]; currentTimeblockRange: { @@ -260,9 +272,14 @@ const dragState = ref({ }); const resizedCurrentBlock = ref<{ start: Date; end: Date } | null>(null); +// Cursor line state +const showCursor = ref(false); +const cursorPosition = ref(null); + // Check if the current date is a weekend (Saturday or Sunday) const isWeekend = computed(() => { - const date = props.currentTimeblockRange.start; + // Use the explicit date prop first, fallback to currentTimeblockRange.start + const date = props.date || props.currentTimeblockRange.start; if (!date) return false; const dayOfWeek = moment(date).day(); // 0 = Sunday, 6 = Saturday @@ -343,8 +360,10 @@ const formatTime = (date: Date): string => { }; // Check if mouse is over an existing block (x is already relative position with scroll) +// Note: This does NOT check the current timeblock because we want to allow +// clicking elsewhere to change the current timeblock to a new time const isOverExistingBlock = (relativeX: number): boolean => { - // Check if over any scheduled block + // Check if over any scheduled block (saved tasks from database) for (const task of props.scheduledBlocks) { const blockLeft = getTimePosition(task._real_start); const blockWidth = getTimeWidth(task._real_start, task._real_end); @@ -353,7 +372,7 @@ const isOverExistingBlock = (relativeX: number): boolean => { } } - // Check if over any other timeblock + // Check if over any other timeblock (from other rows in the modal) for (const tb of otherTimeblocksRendered.value) { const blockLeft = getTimePosition(tb._start); const blockWidth = getTimeWidth(tb._start, tb._end); @@ -362,18 +381,7 @@ const isOverExistingBlock = (relativeX: number): boolean => { } } - // Check if over current timeblock - if (props.currentTimeblockRange.start && props.currentTimeblockRange.end) { - const blockLeft = getTimePosition(props.currentTimeblockRange.start); - const blockWidth = getTimeWidth( - props.currentTimeblockRange.start, - props.currentTimeblockRange.end, - ); - if (relativeX >= blockLeft && relativeX <= blockLeft + blockWidth) { - return true; - } - } - + // Don't check current timeblock - allow changing it by clicking elsewhere return false; }; @@ -394,13 +402,38 @@ const handleMouseMove = (event: MouseEvent) => { return; } - if (!timelineScroll.value) return; + if (!timelineScroll.value || !timelineBar.value) return; - // Get position relative to scroll container, accounting for scroll + // Get position relative to scroll container const rect = timelineScroll.value.getBoundingClientRect(); const scrollLeft = timelineScroll.value.scrollLeft; + + // Calculate X position relative to timeline const x = event.clientX - rect.left + scrollLeft; + // Always calculate cursor position (with 15-min snapping) + const rawTime = positionToTime(x); + const snappedTime = snapToInterval(rawTime); + const cursorHour = snappedTime.getHours() + snappedTime.getMinutes() / 60; + + // Check if cursor is in working hours + const isInWorkHours = + cursorHour >= props.employee.work_start && + cursorHour < props.employee.work_end; + + // Only show cursor line if in working hours + if (isInWorkHours) { + cursorPosition.value = snappedTime; + showCursor.value = true; + } else { + showCursor.value = false; + } + + // Update cursor style based on work hours + if (timelineScroll.value) { + timelineScroll.value.style.cursor = isInWorkHours ? 'crosshair' : 'not-allowed'; + } + // Don't show preview if over existing block (using relative position) if (isOverExistingBlock(x)) { showPreview.value = false; @@ -408,19 +441,13 @@ const handleMouseMove = (event: MouseEvent) => { } // Convert to time and snap - const rawTime = positionToTime(x); - const snappedStart = snapToInterval(rawTime); + const snappedStart = snappedTime; // Calculate default end time (1 hour later) let snappedEnd = moment(snappedStart).add(1, 'hour').toDate(); // Only show preview if start is within work hours - const startHour = snappedStart.getHours() + snappedStart.getMinutes() / 60; - - if ( - startHour >= props.employee.work_start && - startHour < props.employee.work_end - ) { + if (isInWorkHours) { // If the end would exceed work hours, cap it at work_end const endHour = snappedEnd.getHours() + snappedEnd.getMinutes() / 60; @@ -450,23 +477,72 @@ const handleMouseMove = (event: MouseEvent) => { const handleMouseLeave = () => { if (!dragState.value.isStretching && !dragState.value.isResizingCurrent) { showPreview.value = false; + showCursor.value = false; + // Reset cursor style to default crosshair + if (timelineScroll.value) { + timelineScroll.value.style.cursor = 'crosshair'; + } } }; -// Mouse down on timeline bar (not on preview or current block) -const handleMouseDown = (_event: MouseEvent) => { - // This will only trigger if not clicking on preview or current block - // (those have @mousedown.stop) -}; +// Handle mousedown on timeline (including header area) +const handleTimelineMouseDown = (event: MouseEvent) => { + if (isMobile.value) return; + if (!timelineScroll.value || !cursorPosition.value) return; -// Preview block mouse down - start creating/stretching -const handlePreviewMouseDown = (event: MouseEvent) => { - if (!previewBlock.value) return; + // Get the target element + const target = event.target as HTMLElement; + // Don't interfere with resize handles on current timeblock + if (target.classList.contains('resize-handle')) { + return; + } + + // Check if clicking on current timeblock (allow resize handles to work) + if (target.closest('.current-timeblock')) { + return; + } + + const rect = timelineScroll.value.getBoundingClientRect(); + const scrollLeft = timelineScroll.value.scrollLeft; + const x = event.clientX - rect.left + scrollLeft; + + // Don't start creating if over existing block + if (isOverExistingBlock(x)) { + return; + } + + // Check if cursor is in work hours (cursor line must be visible) + if (!showCursor.value || !showPreview.value || !previewBlock.value) { + return; + } + + // Start creating/stretching block at the snapped cursor position dragState.value.isStretching = true; dragState.value.startX = event.clientX; - dragState.value.initialStart = previewBlock.value.start; - dragState.value.initialEnd = previewBlock.value.end; + dragState.value.initialStart = cursorPosition.value; + + // Calculate initial end time (1 hour later, capped at work_end) + let initialEnd = moment(cursorPosition.value).add(1, 'hour').toDate(); + const endHour = initialEnd.getHours() + initialEnd.getMinutes() / 60; + if (endHour > props.employee.work_end) { + initialEnd = moment(cursorPosition.value) + .set({ + hour: Math.floor(props.employee.work_end), + minute: (props.employee.work_end % 1) * 60, + second: 0, + millisecond: 0, + }) + .toDate(); + } + + dragState.value.initialEnd = initialEnd; + + // Set initial preview block (already has 1 hour duration from preview) + previewBlock.value = { + start: cursorPosition.value, + end: initialEnd, + }; event.preventDefault(); }; @@ -484,13 +560,18 @@ const handleStretchMove = (event: MouseEvent) => { const initialStartTime = dragState.value.initialStart.getTime(); const snappedTimeValue = snappedTime.getTime(); - if (snappedTimeValue > initialStartTime) { + if (snappedTimeValue >= initialStartTime) { // Stretching right (extending end) + // Ensure minimum 15-minute block + let endTime = snappedTime; + if (snappedTimeValue === initialStartTime) { + endTime = moment(snappedTime).add(15, 'minutes').toDate(); + } + // Cap at work_end - const snappedEndHour = - snappedTime.getHours() + snappedTime.getMinutes() / 60; - if (snappedEndHour > props.employee.work_end) { - snappedTime = moment(snappedTime) + const endHour = endTime.getHours() + endTime.getMinutes() / 60; + if (endHour > props.employee.work_end) { + endTime = moment(endTime) .set({ hour: Math.floor(props.employee.work_end), minute: (props.employee.work_end % 1) * 60, @@ -502,8 +583,12 @@ const handleStretchMove = (event: MouseEvent) => { previewBlock.value = { start: dragState.value.initialStart, - end: snappedTime, + end: endTime, }; + + // Update cursor to show at the actual block edge (end) + cursorPosition.value = endTime; + showCursor.value = true; } else { // Stretching left (extending start) // Cap at work_start @@ -524,17 +609,27 @@ const handleStretchMove = (event: MouseEvent) => { start: snappedTime, end: dragState.value.initialStart, }; + + // Update cursor to show at the actual block edge (start) + cursorPosition.value = snappedTime; + showCursor.value = true; } }; // Handle stretch end const handleStretchEnd = () => { if (dragState.value.isStretching && previewBlock.value) { - // Emit the new timeblock - emit('timeblockUpdated', { - starts_at_time: formatTime(previewBlock.value.start), - ends_at_time: formatTime(previewBlock.value.end), - }); + // Only emit if we have a valid block (start != end) + const startTime = previewBlock.value.start.getTime(); + const endTime = previewBlock.value.end.getTime(); + + if (endTime > startTime) { + // Emit the new timeblock + emit('timeblockUpdated', { + starts_at_time: formatTime(previewBlock.value.start), + ends_at_time: formatTime(previewBlock.value.end), + }); + } } dragState.value.isStretching = false; @@ -599,6 +694,9 @@ const handleResizeCurrentMove = (event: MouseEvent) => { start: snappedTime, end: dragState.value.initialEnd, }; + // Update cursor to show at the actual block edge + cursorPosition.value = snappedTime; + showCursor.value = true; } } else if (dragState.value.resizeEdge === 'right') { // Resizing right edge (changing end time) @@ -622,6 +720,9 @@ const handleResizeCurrentMove = (event: MouseEvent) => { start: dragState.value.initialStart, end: snappedTime, }; + // Update cursor to show at the actual block edge + cursorPosition.value = snappedTime; + showCursor.value = true; } } }; @@ -709,6 +810,7 @@ defineExpose({ position: relative width: 100% scrollbar-width: none // Firefox + cursor: crosshair &::-webkit-scrollbar display: none // Chrome, Safari, Opera @@ -719,7 +821,6 @@ defineExpose({ width: calc(24 * 60px) background: #fafafa border-radius: 4px - cursor: crosshair @media (max-width: 768px) width: calc(24 * 50px) @@ -792,6 +893,7 @@ defineExpose({ display: flex align-items: center justify-content: center + pointer-events: none &:hover opacity: 0.8 @@ -861,6 +963,15 @@ defineExpose({ z-index: 4 pointer-events: none +.timeline-cursor + position: absolute + top: 0 + bottom: 0 + width: 2px + background: rgba(0, 0, 0, 0.3) + z-index: 10 + pointer-events: none + .timeblock-duration position: absolute top: 50% diff --git a/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock.vue b/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock.vue index de18ea1..b716a7f 100644 --- a/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock.vue +++ b/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock.vue @@ -33,6 +33,7 @@ ref="timelineRef" :is-loading="isLoading" :employee="employee" + :date="modelValue.date" :scheduled-blocks="scheduledBlocks" :other-timeblocks="otherTimeblocksForSameDate" :current-timeblock-range="currentTimeblockRange"