From 33174b6bb1305ab76633a378d482c8f91302e50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Glasn=C3=A1k?= Date: Thu, 11 Dec 2025 01:30:29 +0100 Subject: [PATCH] feat(012): enhance timeblock management with deleted tracking and improved rendering --- .../a-employee-timeblock-timeline.vue | 22 ++--- .../employees/a-employee-timeblock.vue | 86 +++++++++++++------ .../task/_components/employees/a-employee.vue | 6 ++ .../plugins/tm@modal/task/z-task-modal.vue | 30 ++++--- 4 files changed, 97 insertions(+), 47 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 d6bb08e..0b4e732 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 @@ -53,16 +53,16 @@ :title="`${task.subtask?.title || task.blocker?.type || 'Task'}`" > - +
@@ -122,7 +122,7 @@ interface Props { isLoading: boolean; employee: EmployeeFullData; scheduledBlocks: TimelineTask[]; - unsavedTimeblocks?: TimeblockData[]; + otherTimeblocks?: TimeblockData[]; currentTimeblockRange: { start: Date | null; end: Date | null; @@ -140,15 +140,15 @@ interface Props { } const props = withDefaults(defineProps(), { - unsavedTimeblocks: () => [], + otherTimeblocks: () => [], }); const timelineContainer = ref(null); const timelineScroll = ref(null); -// Convert unsaved timeblocks to renderable format -const unsavedTimeblocksRendered = computed(() => { - return props.unsavedTimeblocks +// Convert other timeblocks to renderable format +const otherTimeblocksRendered = computed(() => { + return props.otherTimeblocks .map((tb) => { if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return null; @@ -255,7 +255,7 @@ defineExpose({ border-radius: 2px z-index: 2 -.unsaved-timeblock +.other-timeblock position: absolute top: 5px height: 22px 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 f9bfa08..c8bcd5b 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 @@ -31,7 +31,7 @@ :is-loading="isLoading" :employee="employee" :scheduled-blocks="scheduledBlocks" - :unsaved-timeblocks="unsavedTimeblocksForSameDate" + :other-timeblocks="otherTimeblocksForSameDate" :current-timeblock-range="currentTimeblockRange" :collision="collision" :collision-segments="collisionSegments" @@ -48,19 +48,25 @@ import { computed, ref, watch, onMounted, nextTick } from 'vue'; import { useVuelidate } from '@vuelidate/core'; import { helpers } from '@vuelidate/validators'; import { useEmployeeTimeline } from '@/plugins/tm@_utils/useEmployeeTimeline'; -import { timeblockToDateRange } from '@/plugins/tm@_utils/timeblock.utils'; +import { + timeblockToDateRange, + detectCollision, + getCollisionSegments as getCollisionSegmentsUtil, +} from '@/plugins/tm@_utils/timeblock.utils'; import AEmployeeTimeblockHeader from './a-employee-timeblock-header.vue'; import AEmployeeTimeblockTimeline from './a-employee-timeblock-timeline.vue'; import moment from 'moment'; import type { TimeblockData, EmployeeFullData } from './types'; // Configuration: Set to false to allow editing timeblocks with existing IDs -const ALLOW_EDITING_EXISTING_TIMEBLOCKS = false; +const ALLOW_EDITING_EXISTING_TIMEBLOCKS = true; interface Props { modelValue: TimeblockData; employee: EmployeeFullData | null; - allEmployeeTimeblocks?: TimeblockData[]; + allEmployeeTimeblocks?: TimeblockData[]; // Timeblocks for current employee + allTimeblocks?: TimeblockData[]; // All timeblocks from all employees + deletedTimeblockIds?: number[]; // IDs of saved timeblocks that have been deleted currentIndex?: number; deletable?: boolean; index?: number; @@ -68,6 +74,8 @@ interface Props { const props = withDefaults(defineProps(), { allEmployeeTimeblocks: () => [], + allTimeblocks: () => [], + deletedTimeblockIds: () => [], currentIndex: 0, deletable: false, index: 0, @@ -212,8 +220,6 @@ const { timelineTasks, scheduledTime: _scheduledTime, freeTime: _freeTime, - collision, - collisionSegments, timeblockDuration, refetch, } = useEmployeeTimeline(employeeRef, dateRef, timeblockRef); @@ -229,35 +235,63 @@ watch( ); // All tasks from timelineTasks are already filtered for the selected date +// Filter out timeblocks that are being edited or deleted locally const scheduledBlocks = computed(() => { - return timelineTasks.value; + // Combine IDs that should be filtered: edited locally or deleted + const hiddenTimeblockIds = new Set([ + // Timeblock IDs being edited locally + ...props.allTimeblocks.filter(tb => tb.id).map(tb => tb.id), + // Timeblock IDs that have been deleted + ...(props.deletedTimeblockIds || []) + ]); + + // Filter out tasks whose timeblock IDs should be hidden + return timelineTasks.value.filter(task => + !task._timeblock_id || !hiddenTimeblockIds.has(task._timeblock_id) + ); }); -// Get other unsaved timeblocks for the same date (excluding current one) -const unsavedTimeblocksForSameDate = computed(() => { - if (!props.modelValue.date || !props.allEmployeeTimeblocks) return []; +// 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); +}); + +// Recalculate collision segments using filtered scheduled blocks +const collisionSegments = computed(() => { + return getCollisionSegmentsUtil(props.modelValue, scheduledBlocks.value); +}); + +// Get other timeblocks for the same date (excluding current one) +// This includes timeblocks from both the current employee and other employees +const otherTimeblocksForSameDate = computed(() => { + if (!props.modelValue.date) return []; const currentDate = moment(props.modelValue.date).format('YYYY-MM-DD'); + const currentTimeblock = props.modelValue; - return props.allEmployeeTimeblocks - .filter((tb, index) => { - // Exclude the current timeblock - if (index === props.currentIndex) return false; + // Use allTimeblocks if available (includes all employees), otherwise fall back to current employee only + const timeblocksToCheck = props.allTimeblocks.length > 0 + ? props.allTimeblocks + : props.allEmployeeTimeblocks; - // Only include timeblocks WITHOUT IDs (unsaved) - if (tb.id) return false; + return timeblocksToCheck.filter(tb => { + // Skip current timeblock - compare by reference or by matching all key properties + if (tb === currentTimeblock || + (tb.employee_id === currentTimeblock.employee_id && + tb.date === currentTimeblock.date && + tb.starts_at_time === currentTimeblock.starts_at_time && + tb.ends_at_time === currentTimeblock.ends_at_time && + tb.id === currentTimeblock.id)) { + return false; + } - // Must have a date - if (!tb.date) return false; + // Must have required fields + if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return false; - // Must be the same date - const tbDate = moment(tb.date).format('YYYY-MM-DD'); - return tbDate === currentDate; - }) - .filter((tb) => { - // Only include if it has valid time range - return tb.starts_at_time && tb.ends_at_time; - }); + // Must be the same date + return moment(tb.date).format('YYYY-MM-DD') === currentDate; + }); }); // Get current timeblock range diff --git a/app/src/plugins/tm@modal/task/_components/employees/a-employee.vue b/app/src/plugins/tm@modal/task/_components/employees/a-employee.vue index c8d0327..eee993e 100644 --- a/app/src/plugins/tm@modal/task/_components/employees/a-employee.vue +++ b/app/src/plugins/tm@modal/task/_components/employees/a-employee.vue @@ -17,6 +17,8 @@ :model-value="timeblock" :employee="selectedEmployee" :all-employee-timeblocks="timeblocks" + :all-timeblocks="allTimeblocks" + :deleted-timeblock-ids="deletedTimeblockIds" :current-index="index" :deletable="timeblocks.length > 1" :index="index" @@ -42,6 +44,8 @@ import type { interface Props { employeeData: EmployeeData; timeblocks?: TimeblockData[]; + allTimeblocks?: TimeblockData[]; // All timeblocks from all employees + deletedTimeblockIds?: number[]; // IDs of saved timeblocks that have been deleted customFilter?: CustomFilterFunction; hasBackground?: boolean; required?: boolean; @@ -49,6 +53,8 @@ interface Props { const props = withDefaults(defineProps(), { timeblocks: () => [], + allTimeblocks: () => [], + deletedTimeblockIds: () => [], customFilter: undefined, hasBackground: true, required: false, 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 7247418..887b5df 100644 --- a/app/src/plugins/tm@modal/task/z-task-modal.vue +++ b/app/src/plugins/tm@modal/task/z-task-modal.vue @@ -100,6 +100,8 @@ v-if="subtask.id" v-model:employee-data="editModeEmployee" :timeblocks="getEmployeeTimeblocks(subtask.employee_id)" + :all-timeblocks="timeblocks" + :deleted-timeblock-ids="deletedTimeblockIds" :custom-filter="customFilter" @update:timeblocks=" updateEmployeeTimeblocks($event, subtask.employee_id) @@ -118,6 +120,8 @@ :ref="(el) => setEmployeeRef(el, index)" v-model:employee-data="subtask.employees[index]" :timeblocks="getEmployeeTimeblocks(employee.id)" + :all-timeblocks="timeblocks" + :deleted-timeblock-ids="deletedTimeblockIds" :custom-filter="customFilter" :required="index === 0" @update:timeblocks="updateEmployeeTimeblocks($event, employee.id)" @@ -315,6 +319,7 @@ export default { showDatePicker: false, selectedNewDate: [], timeblocks: [], + deletedTimeblockIds: [], // Track IDs of saved timeblocks that have been deleted wasFormValidationTriggered: false, modalName: 'z-task-modal', employeeRefs: [], @@ -537,16 +542,20 @@ export default { }, updateEmployeeTimeblocks(updatedTimeblocks, employeeId) { - // Remove old timeblocks for this employee - const otherTimeblocks = this.timeblocks.filter( - (tb) => tb.employee_id !== employeeId, - ); - // Add updated timeblocks with employee_id - const timeblocksWithEmployeeId = updatedTimeblocks.map((tb) => ({ - ...tb, - employee_id: employeeId, - })); - this.timeblocks = [...otherTimeblocks, ...timeblocksWithEmployeeId]; + // Track deleted timeblocks (saved timeblocks that are no longer in the updated list) + const updatedIds = new Set(updatedTimeblocks.filter(tb => tb.id).map(tb => tb.id)); + const deletedIds = this.timeblocks + .filter(tb => tb.employee_id === employeeId && tb.id && !updatedIds.has(tb.id)) + .map(tb => tb.id) + .filter(id => !this.deletedTimeblockIds.includes(id)); + + this.deletedTimeblockIds.push(...deletedIds); + + // Replace timeblocks for this employee + const otherEmployeeTimeblocks = this.timeblocks.filter(tb => tb.employee_id !== employeeId); + const updatedWithEmployeeId = updatedTimeblocks.map(tb => ({ ...tb, employee_id: employeeId })); + + this.timeblocks = [...otherEmployeeTimeblocks, ...updatedWithEmployeeId]; }, updateTimeblock(val, index, employeeId) { @@ -655,6 +664,7 @@ export default { this.visible = true; this.timeblocks = []; + this.deletedTimeblockIds = []; // Reset deleted timeblock IDs this.employeeRefs = []; this.$nextTick(() => { this.v$.$reset();