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();