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
+95 -45

No files matched your search

@@ -53,16 +53,16 @@
:title="`${task.subtask?.title || task.blocker?.type || 'Task'}`"
></div>
<!-- Unsaved timeblocks for same date -->
<!-- Other timeblocks for same date -->
<div
v-for="(timeblock, index) in unsavedTimeblocksRendered"
:key="`unsaved-${index}`"
class="unsaved-timeblock"
v-for="(timeblock, index) in otherTimeblocksRendered"
:key="`other-${index}`"
class="other-timeblock"
:style="{
left: `${getTimePosition(timeblock._start)}px`,
width: `${getTimeWidth(timeblock._start, timeblock._end)}px`,
}"
:title="`Unsaved timeblock`"
:title="`Other timeblock`"
></div>
<!-- Current timeblock (if valid) -->
@@ -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<Props>(), {
unsavedTimeblocks: () => [],
otherTimeblocks: () => [],
});
const timelineContainer = ref<HTMLElement | null>(null);
const timelineScroll = ref<HTMLElement | null>(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
@@ -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<Props>(), {
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,34 +235,62 @@ 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;
return moment(tb.date).format('YYYY-MM-DD') === currentDate;
});
});
@@ -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<Props>(), {
timeblocks: () => [],
allTimeblocks: () => [],
deletedTimeblockIds: () => [],
customFilter: undefined,
hasBackground: true,
required: false,
+20 -10
View File
@@ -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();