feat(012): enhance timeblock management with deleted tracking and improved rendering

This commit is contained in:
Matej Glasnák committed 2025-12-11 01:30:29 +01:00
1 parent fd74a0fe29
commit 33174b6bb1
4 files changed
+97 -47

No files matched your search

@@ -53,16 +53,16 @@
:title="`${task.subtask?.title || task.blocker?.type || 'Task'}`" :title="`${task.subtask?.title || task.blocker?.type || 'Task'}`"
></div> ></div>
<!-- Unsaved timeblocks for same date --> <!-- Other timeblocks for same date -->
<div <div
v-for="(timeblock, index) in unsavedTimeblocksRendered" v-for="(timeblock, index) in otherTimeblocksRendered"
:key="`unsaved-${index}`" :key="`other-${index}`"
class="unsaved-timeblock" class="other-timeblock"
:style="{ :style="{
left: `${getTimePosition(timeblock._start)}px`, left: `${getTimePosition(timeblock._start)}px`,
width: `${getTimeWidth(timeblock._start, timeblock._end)}px`, width: `${getTimeWidth(timeblock._start, timeblock._end)}px`,
}" }"
:title="`Unsaved timeblock`" :title="`Other timeblock`"
></div> ></div>
<!-- Current timeblock (if valid) --> <!-- Current timeblock (if valid) -->
@@ -122,7 +122,7 @@ interface Props {
isLoading: boolean; isLoading: boolean;
employee: EmployeeFullData; employee: EmployeeFullData;
scheduledBlocks: TimelineTask[]; scheduledBlocks: TimelineTask[];
unsavedTimeblocks?: TimeblockData[]; otherTimeblocks?: TimeblockData[];
currentTimeblockRange: { currentTimeblockRange: {
start: Date | null; start: Date | null;
end: Date | null; end: Date | null;
@@ -140,15 +140,15 @@ interface Props {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
unsavedTimeblocks: () => [], otherTimeblocks: () => [],
}); });
const timelineContainer = ref<HTMLElement | null>(null); const timelineContainer = ref<HTMLElement | null>(null);
const timelineScroll = ref<HTMLElement | null>(null); const timelineScroll = ref<HTMLElement | null>(null);
// Convert unsaved timeblocks to renderable format // Convert other timeblocks to renderable format
const unsavedTimeblocksRendered = computed(() => { const otherTimeblocksRendered = computed(() => {
return props.unsavedTimeblocks return props.otherTimeblocks
.map((tb) => { .map((tb) => {
if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return null; if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return null;
@@ -255,7 +255,7 @@ defineExpose({
border-radius: 2px border-radius: 2px
z-index: 2 z-index: 2
.unsaved-timeblock .other-timeblock
position: absolute position: absolute
top: 5px top: 5px
height: 22px height: 22px
@@ -31,7 +31,7 @@
:is-loading="isLoading" :is-loading="isLoading"
:employee="employee" :employee="employee"
:scheduled-blocks="scheduledBlocks" :scheduled-blocks="scheduledBlocks"
:unsaved-timeblocks="unsavedTimeblocksForSameDate" :other-timeblocks="otherTimeblocksForSameDate"
:current-timeblock-range="currentTimeblockRange" :current-timeblock-range="currentTimeblockRange"
:collision="collision" :collision="collision"
:collision-segments="collisionSegments" :collision-segments="collisionSegments"
@@ -48,19 +48,25 @@ import { computed, ref, watch, onMounted, nextTick } from 'vue';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { helpers } from '@vuelidate/validators'; import { helpers } from '@vuelidate/validators';
import { useEmployeeTimeline } from '@/plugins/tm@_utils/useEmployeeTimeline'; 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 AEmployeeTimeblockHeader from './a-employee-timeblock-header.vue';
import AEmployeeTimeblockTimeline from './a-employee-timeblock-timeline.vue'; import AEmployeeTimeblockTimeline from './a-employee-timeblock-timeline.vue';
import moment from 'moment'; import moment from 'moment';
import type { TimeblockData, EmployeeFullData } from './types'; import type { TimeblockData, EmployeeFullData } from './types';
// Configuration: Set to false to allow editing timeblocks with existing IDs // 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 { interface Props {
modelValue: TimeblockData; modelValue: TimeblockData;
employee: EmployeeFullData | null; 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; currentIndex?: number;
deletable?: boolean; deletable?: boolean;
index?: number; index?: number;
@@ -68,6 +74,8 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
allEmployeeTimeblocks: () => [], allEmployeeTimeblocks: () => [],
allTimeblocks: () => [],
deletedTimeblockIds: () => [],
currentIndex: 0, currentIndex: 0,
deletable: false, deletable: false,
index: 0, index: 0,
@@ -212,8 +220,6 @@ const {
timelineTasks, timelineTasks,
scheduledTime: _scheduledTime, scheduledTime: _scheduledTime,
freeTime: _freeTime, freeTime: _freeTime,
collision,
collisionSegments,
timeblockDuration, timeblockDuration,
refetch, refetch,
} = useEmployeeTimeline(employeeRef, dateRef, timeblockRef); } = useEmployeeTimeline(employeeRef, dateRef, timeblockRef);
@@ -229,35 +235,63 @@ watch(
); );
// All tasks from timelineTasks are already filtered for the selected date // All tasks from timelineTasks are already filtered for the selected date
// Filter out timeblocks that are being edited or deleted locally
const scheduledBlocks = computed(() => { 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) // Recalculate collision using filtered scheduled blocks (not raw timeline tasks)
const unsavedTimeblocksForSameDate = computed(() => { // This prevents false collisions with timeblocks being edited
if (!props.modelValue.date || !props.allEmployeeTimeblocks) return []; 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 currentDate = moment(props.modelValue.date).format('YYYY-MM-DD');
const currentTimeblock = props.modelValue;
return props.allEmployeeTimeblocks // Use allTimeblocks if available (includes all employees), otherwise fall back to current employee only
.filter((tb, index) => { const timeblocksToCheck = props.allTimeblocks.length > 0
// Exclude the current timeblock ? props.allTimeblocks
if (index === props.currentIndex) return false; : props.allEmployeeTimeblocks;
// Only include timeblocks WITHOUT IDs (unsaved) return timeblocksToCheck.filter(tb => {
if (tb.id) return false; // 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 // Must have required fields
if (!tb.date) return false; if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return false;
// Must be the same date // Must be the same date
const tbDate = moment(tb.date).format('YYYY-MM-DD'); return moment(tb.date).format('YYYY-MM-DD') === currentDate;
return tbDate === currentDate; });
})
.filter((tb) => {
// Only include if it has valid time range
return tb.starts_at_time && tb.ends_at_time;
});
}); });
// Get current timeblock range // Get current timeblock range
@@ -17,6 +17,8 @@
:model-value="timeblock" :model-value="timeblock"
:employee="selectedEmployee" :employee="selectedEmployee"
:all-employee-timeblocks="timeblocks" :all-employee-timeblocks="timeblocks"
:all-timeblocks="allTimeblocks"
:deleted-timeblock-ids="deletedTimeblockIds"
:current-index="index" :current-index="index"
:deletable="timeblocks.length > 1" :deletable="timeblocks.length > 1"
:index="index" :index="index"
@@ -42,6 +44,8 @@ import type {
interface Props { interface Props {
employeeData: EmployeeData; employeeData: EmployeeData;
timeblocks?: TimeblockData[]; timeblocks?: TimeblockData[];
allTimeblocks?: TimeblockData[]; // All timeblocks from all employees
deletedTimeblockIds?: number[]; // IDs of saved timeblocks that have been deleted
customFilter?: CustomFilterFunction; customFilter?: CustomFilterFunction;
hasBackground?: boolean; hasBackground?: boolean;
required?: boolean; required?: boolean;
@@ -49,6 +53,8 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
timeblocks: () => [], timeblocks: () => [],
allTimeblocks: () => [],
deletedTimeblockIds: () => [],
customFilter: undefined, customFilter: undefined,
hasBackground: true, hasBackground: true,
required: false, required: false,
+20 -10
View File
@@ -100,6 +100,8 @@
v-if="subtask.id" v-if="subtask.id"
v-model:employee-data="editModeEmployee" v-model:employee-data="editModeEmployee"
:timeblocks="getEmployeeTimeblocks(subtask.employee_id)" :timeblocks="getEmployeeTimeblocks(subtask.employee_id)"
:all-timeblocks="timeblocks"
:deleted-timeblock-ids="deletedTimeblockIds"
:custom-filter="customFilter" :custom-filter="customFilter"
@update:timeblocks=" @update:timeblocks="
updateEmployeeTimeblocks($event, subtask.employee_id) updateEmployeeTimeblocks($event, subtask.employee_id)
@@ -118,6 +120,8 @@
:ref="(el) => setEmployeeRef(el, index)" :ref="(el) => setEmployeeRef(el, index)"
v-model:employee-data="subtask.employees[index]" v-model:employee-data="subtask.employees[index]"
:timeblocks="getEmployeeTimeblocks(employee.id)" :timeblocks="getEmployeeTimeblocks(employee.id)"
:all-timeblocks="timeblocks"
:deleted-timeblock-ids="deletedTimeblockIds"
:custom-filter="customFilter" :custom-filter="customFilter"
:required="index === 0" :required="index === 0"
@update:timeblocks="updateEmployeeTimeblocks($event, employee.id)" @update:timeblocks="updateEmployeeTimeblocks($event, employee.id)"
@@ -315,6 +319,7 @@ export default {
showDatePicker: false, showDatePicker: false,
selectedNewDate: [], selectedNewDate: [],
timeblocks: [], timeblocks: [],
deletedTimeblockIds: [], // Track IDs of saved timeblocks that have been deleted
wasFormValidationTriggered: false, wasFormValidationTriggered: false,
modalName: 'z-task-modal', modalName: 'z-task-modal',
employeeRefs: [], employeeRefs: [],
@@ -537,16 +542,20 @@ export default {
}, },
updateEmployeeTimeblocks(updatedTimeblocks, employeeId) { updateEmployeeTimeblocks(updatedTimeblocks, employeeId) {
// Remove old timeblocks for this employee // Track deleted timeblocks (saved timeblocks that are no longer in the updated list)
const otherTimeblocks = this.timeblocks.filter( const updatedIds = new Set(updatedTimeblocks.filter(tb => tb.id).map(tb => tb.id));
(tb) => tb.employee_id !== employeeId, const deletedIds = this.timeblocks
); .filter(tb => tb.employee_id === employeeId && tb.id && !updatedIds.has(tb.id))
// Add updated timeblocks with employee_id .map(tb => tb.id)
const timeblocksWithEmployeeId = updatedTimeblocks.map((tb) => ({ .filter(id => !this.deletedTimeblockIds.includes(id));
...tb,
employee_id: employeeId, this.deletedTimeblockIds.push(...deletedIds);
}));
this.timeblocks = [...otherTimeblocks, ...timeblocksWithEmployeeId]; // 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) { updateTimeblock(val, index, employeeId) {
@@ -655,6 +664,7 @@ export default {
this.visible = true; this.visible = true;
this.timeblocks = []; this.timeblocks = [];
this.deletedTimeblockIds = []; // Reset deleted timeblock IDs
this.employeeRefs = []; this.employeeRefs = [];
this.$nextTick(() => { this.$nextTick(() => {
this.v$.$reset(); this.v$.$reset();