From 1b666a63a368b42e4d63e26f57c3fda076e915d5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matej=20Glasn=C3=A1k?=
Date: Thu, 8 Jan 2026 00:45:01 +0100
Subject: [PATCH] feat(timeblock): enhance timeblock components with color
indication for negative free time and improved time constraints for employee
schedulingd
---
.../employees/a-employee-timeblock-header.vue | 4 +-
.../a-employee-timeblock-timeline.vue | 84 +++++++++++++++++--
.../employees/a-employee-timeblock.vue | 28 +++++++
3 files changed, 109 insertions(+), 7 deletions(-)
diff --git a/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-header.vue b/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-header.vue
index 2a08bcb..2eccda9 100644
--- a/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-header.vue
+++ b/app/src/plugins/tm@modal/task/_components/employees/a-employee-timeblock-header.vue
@@ -117,7 +117,9 @@
class="timeblock__time-overview__label timeblock__time-overview__label--mobile"
>Vol. čas:
- {{ freeTime }}h
+ {{ freeTime }}h
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 7eea973..43aa3fb 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
@@ -398,16 +398,32 @@ const handleMouseMove = (event: MouseEvent) => {
// Convert to time and snap
const rawTime = positionToTime(x);
const snappedStart = snapToInterval(rawTime);
- const snappedEnd = moment(snappedStart).add(1, 'hour').toDate();
- // Only show preview within work hours
+ // 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;
- const endHour = snappedEnd.getHours() + snappedEnd.getMinutes() / 60;
if (
startHour >= props.employee.work_start &&
- endHour <= props.employee.work_end
+ startHour < props.employee.work_end
) {
+ // If the end would exceed work hours, cap it at work_end
+ const endHour = snappedEnd.getHours() + snappedEnd.getMinutes() / 60;
+
+ if (endHour > props.employee.work_end) {
+ // Create end time at exactly work_end
+ snappedEnd = moment(snappedStart)
+ .set({
+ hour: Math.floor(props.employee.work_end),
+ minute: (props.employee.work_end % 1) * 60,
+ second: 0,
+ millisecond: 0,
+ })
+ .toDate();
+ }
+
showPreview.value = true;
previewBlock.value = {
start: snappedStart,
@@ -451,19 +467,47 @@ const handleStretchMove = (event: MouseEvent) => {
const x = event.clientX - rect.left + timelineScroll.value.scrollLeft;
const currentTime = positionToTime(x);
- const snappedTime = snapToInterval(currentTime);
+ let snappedTime = snapToInterval(currentTime);
const initialStartTime = dragState.value.initialStart.getTime();
const snappedTimeValue = snappedTime.getTime();
if (snappedTimeValue > initialStartTime) {
// Stretching right (extending end)
+ // Cap at work_end
+ const snappedEndHour =
+ snappedTime.getHours() + snappedTime.getMinutes() / 60;
+ if (snappedEndHour > props.employee.work_end) {
+ snappedTime = moment(snappedTime)
+ .set({
+ hour: Math.floor(props.employee.work_end),
+ minute: (props.employee.work_end % 1) * 60,
+ second: 0,
+ millisecond: 0,
+ })
+ .toDate();
+ }
+
previewBlock.value = {
start: dragState.value.initialStart,
end: snappedTime,
};
} else {
// Stretching left (extending start)
+ // Cap at work_start
+ const snappedStartHour =
+ snappedTime.getHours() + snappedTime.getMinutes() / 60;
+ if (snappedStartHour < props.employee.work_start) {
+ snappedTime = moment(snappedTime)
+ .set({
+ hour: Math.floor(props.employee.work_start),
+ minute: (props.employee.work_start % 1) * 60,
+ second: 0,
+ millisecond: 0,
+ })
+ .toDate();
+ }
+
previewBlock.value = {
start: snappedTime,
end: dragState.value.initialStart,
@@ -519,10 +563,24 @@ const handleResizeCurrentMove = (event: MouseEvent) => {
const x = event.clientX - rect.left + timelineScroll.value.scrollLeft;
const currentTime = positionToTime(x);
- const snappedTime = snapToInterval(currentTime);
+ let snappedTime = snapToInterval(currentTime);
if (dragState.value.resizeEdge === 'left') {
// Resizing left edge (changing start time)
+ // Cap at work_start
+ const snappedStartHour =
+ snappedTime.getHours() + snappedTime.getMinutes() / 60;
+ if (snappedStartHour < props.employee.work_start) {
+ snappedTime = moment(snappedTime)
+ .set({
+ hour: Math.floor(props.employee.work_start),
+ minute: (props.employee.work_start % 1) * 60,
+ second: 0,
+ millisecond: 0,
+ })
+ .toDate();
+ }
+
// Make sure new start is before end
if (snappedTime < dragState.value.initialEnd) {
resizedCurrentBlock.value = {
@@ -532,6 +590,20 @@ const handleResizeCurrentMove = (event: MouseEvent) => {
}
} else if (dragState.value.resizeEdge === 'right') {
// Resizing right edge (changing end time)
+ // Cap at work_end
+ const snappedEndHour =
+ snappedTime.getHours() + snappedTime.getMinutes() / 60;
+ if (snappedEndHour > props.employee.work_end) {
+ snappedTime = moment(snappedTime)
+ .set({
+ hour: Math.floor(props.employee.work_end),
+ minute: (props.employee.work_end % 1) * 60,
+ second: 0,
+ millisecond: 0,
+ })
+ .toDate();
+ }
+
// Make sure new end is after start
if (snappedTime > dragState.value.initialStart) {
resizedCurrentBlock.value = {
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 191f4a4..182b5d9 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
@@ -426,6 +426,34 @@ const validate = async () => {
defineExpose({
validate,
});
+
+watch(
+ () => props.modelValue.ends_at_time,
+ () => {
+ if (v$.value.ends_at_time.$model !== props.modelValue.ends_at_time) {
+ v$.value.ends_at_time.$model = props.modelValue.ends_at_time;
+ }
+ },
+);
+
+watch(
+ () => props.modelValue.starts_at_time,
+ () => {
+ if (v$.value.starts_at_time.$model !== props.modelValue.starts_at_time) {
+ v$.value.starts_at_time.$model = props.modelValue.starts_at_time;
+ console.log('starts_at_time changed', v$.value.starts_at_time.$model);
+ }
+ },
+);
+
+watch(
+ () => props.modelValue.date,
+ () => {
+ if (v$.value.date.$model !== props.modelValue.date) {
+ v$.value.date.$model = props.modelValue.date;
+ }
+ },
+);