From 198f50dfc1ccd979f70bd70464285cc78b9721d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Glasn=C3=A1k?= Date: Thu, 8 Jan 2026 01:13:06 +0100 Subject: [PATCH] feat(z-task-modal): improve employee selection logic and add weekend overlay in timeblock components --- .../a-employee-timeblock-timeline.vue | 23 ++++++++++ .../employees/a-employee-timeblock.vue | 36 +++++++++++++--- .../plugins/tm@modal/task/z-task-modal.vue | 42 ++++++++++++------- 3 files changed, 81 insertions(+), 20 deletions(-) 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 43aa3fb..423c6d7 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 @@ -47,6 +47,9 @@ }" > + +
+
(null); +// Check if the current date is a weekend (Saturday or Sunday) +const isWeekend = computed(() => { + const date = props.currentTimeblockRange.start; + if (!date) return false; + + const dayOfWeek = moment(date).day(); // 0 = Sunday, 6 = Saturday + return dayOfWeek === 0 || dayOfWeek === 6; +}); + // Convert other timeblocks to renderable format const otherTimeblocksRendered = computed(() => { return props.otherTimeblocks @@ -728,6 +740,17 @@ defineExpose({ z-index: 1 pointer-events: none +.weekend-overlay + position: absolute + top: 0 + left: 0 + width: 100% + height: 100% + background: rgba(255, 124, 124, 0.3) !important + z-index: 1 + pointer-events: none + border-radius: 4px + .scheduled-block position: absolute top: 5px 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 182b5d9..7022124 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 @@ -281,15 +281,39 @@ const scheduledBlocks = computed(() => { ); }); -// Recalculate collision using filtered scheduled blocks (not raw timeline tasks) -// This prevents false collisions with timeblocks being edited -const collision = computed(() => { - return detectCollision(props.modelValue, scheduledBlocks.value); +// Convert other unsaved timeblocks to task format for collision detection +const otherTimeblocksAsTasks = computed(() => { + return otherTimeblocksForSameDate.value + .map((tb) => { + const { start, end } = timeblockToDateRange(tb); + if (!start || !end) return null; + + return { + _real_start: start, + _real_end: end, + _timeblock_id: tb.id || null, + }; + }) + .filter((task) => task !== null); }); -// Recalculate collision segments using filtered scheduled blocks +// Combine scheduled blocks with other unsaved timeblocks for collision detection +const allBlocksForCollisionCheck = computed(() => { + return [...scheduledBlocks.value, ...otherTimeblocksAsTasks.value]; +}); + +// Recalculate collision using filtered scheduled blocks AND other unsaved timeblocks +// This prevents false collisions with timeblocks being edited, but catches collisions with other unsaved timeblocks +const collision = computed(() => { + return detectCollision(props.modelValue, allBlocksForCollisionCheck.value); +}); + +// Recalculate collision segments using filtered scheduled blocks AND other unsaved timeblocks const collisionSegments = computed(() => { - return getCollisionSegmentsUtil(props.modelValue, scheduledBlocks.value); + return getCollisionSegmentsUtil( + props.modelValue, + allBlocksForCollisionCheck.value, + ); }); // Get current timeblock range diff --git a/app/src/plugins/tm@modal/task/z-task-modal.vue b/app/src/plugins/tm@modal/task/z-task-modal.vue index f22498e..b3afea8 100644 --- a/app/src/plugins/tm@modal/task/z-task-modal.vue +++ b/app/src/plugins/tm@modal/task/z-task-modal.vue @@ -531,19 +531,20 @@ export default { const employeeId = itemObject.value; if (!this.subtask.id) { - // New subtask with multiple employees support or edit of draft with multiple employees - return ( - shouldShowItem && - !this.timeblocks.some((tb) => tb.employee_id === employeeId) + // New subtask with multiple employees support + // Hide employees that are already selected in any slot + // The v-autocomplete will still display the currently selected employee + const isAlreadySelected = this.subtask.employees.some( + (emp) => emp.id && Number(emp.id) === employeeId, ); + return shouldShowItem && !isAlreadySelected; } else if (this.subtask.is_draft) { // Edit draft subtask with multiple employees support - return ( - shouldShowItem && - !this.subtask.employees.some( - (employee) => Number(employee.id) === employeeId, - ) + // Hide employees that are already selected in any slot + const isAlreadySelected = this.subtask.employees.some( + (emp) => emp.id && Number(emp.id) === employeeId, ); + return shouldShowItem && !isAlreadySelected; } else { // Saved standard subtask, allow "re-select" for better UX return shouldShowItem; @@ -1046,8 +1047,9 @@ export default { // For drafts with multiple employees, include employees array if (this.subtask.is_draft && this.isDraftWithMultipleEmployees) { - const validEmployees = this.subtask.employees.filter( - (employee) => employee.id && employee.duration, + const validEmployees = this._filterValidEmployees( + this.subtask.employees, + concept, ); // Assign timeblocks to each employee @@ -1090,9 +1092,9 @@ export default { this.$store.commit('tm/update_subtask', subtask); this.$events.emit('on-subtask-edited', this.subtask); } else { - // Filter out employees that don't have both id and duration - const validEmployees = this.subtask.employees.filter( - (employee) => employee.id && employee.duration, + const validEmployees = this._filterValidEmployees( + this.subtask.employees, + concept, ); // Assign timeblocks to each employee based on employee_id @@ -1198,6 +1200,18 @@ export default { } }, + _filterValidEmployees(employees, isConcept) { + // Filter employees based on whether it's a concept/draft or published task + // For concepts: only require id (duration can be null/0) + // For published tasks: require both id and duration + return employees.filter((employee) => { + if (isConcept) { + return employee.id; // Only require ID for concepts/drafts + } + return employee.id && employee.duration; // Require both for published tasks + }); + }, + async _validateEmployees() { if (this.subtask.id && !this.subtask.is_draft) { // For existing subtasks, at least one employee is required