This commit is contained in:
Matej Glasnák committed 2025-12-11 01:57:09 +01:00
1 parent 33174b6bb1
commit 8d8c18206c
4 files changed
+480 -152

No files matched your search

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/vue-query'; import { useQuery } from '@tanstack/vue-query';
import { computed, Ref, unref, watch } from 'vue'; import { computed, Ref, unref } from 'vue';
import axios from '@/plugins/w@custom/axios/models/axios'; import axios from '@/plugins/w@custom/axios/models/axios';
import moment from 'moment'; import moment from 'moment';
import timelineUtils from '@/plugins/tm@dashboard/tasks/_utils/timeline.utils'; import timelineUtils from '@/plugins/tm@dashboard/tasks/_utils/timeline.utils';
@@ -80,8 +80,12 @@ export function useEmployeeTimeline(
const tasks: any[] = []; const tasks: any[] = [];
subtasks.forEach((subtask) => { subtasks.forEach((subtask) => {
// Check if this is a blocker/adhoc/etc with direct start/end fields (no timeblocks array)
if ( if (
![ !subtask.timeblocks &&
subtask.start &&
subtask.end &&
[
'blocker', 'blocker',
'adhoc', 'adhoc',
'home_office', 'home_office',
@@ -90,6 +94,31 @@ export function useEmployeeTimeline(
'office365_blocker', 'office365_blocker',
].includes(subtask.type) ].includes(subtask.type)
) { ) {
const blockerDateStr = moment(subtask.start).format('YYYY-MM-DD');
if (blockerDateStr === selectedDateStr) {
const type = subtask.type;
const task = {
id: subtask.id,
start: new Date(subtask.start),
end: new Date(subtask.end),
master_task: null,
date: new Date(subtask.start),
project_code: null,
subtask: subtask,
blocker: subtask,
type,
_timeblock_id: null,
_timeblock_is_finished: subtask.is_finished || false,
_real_start: new Date(subtask.start),
_real_end: new Date(subtask.end),
};
tasks.push(task);
}
} else {
// Process regular subtasks with timeblocks array
subtask.timeblocks?.forEach((timeblock: any) => { subtask.timeblocks?.forEach((timeblock: any) => {
const timeblockDateStr = moment(timeblock.start).format( const timeblockDateStr = moment(timeblock.start).format(
'YYYY-MM-DD', 'YYYY-MM-DD',
@@ -100,7 +129,7 @@ export function useEmployeeTimeline(
let type = 'subtask'; let type = 'subtask';
if (subtask.type === 'office365') type = 'office365'; if (subtask.type === 'office365') type = 'office365';
tasks.push({ const task = {
id: subtask.id, id: subtask.id,
start: new Date(timeblock.start), start: new Date(timeblock.start),
end: new Date(timeblock.end), end: new Date(timeblock.end),
@@ -108,12 +137,15 @@ export function useEmployeeTimeline(
date: new Date(timeblock.start), date: new Date(timeblock.start),
project_code: subtask.project_code, project_code: subtask.project_code,
subtask: subtask, subtask: subtask,
blocker: undefined,
type, type,
_timeblock_id: timeblock.id, _timeblock_id: timeblock.id,
_timeblock_is_finished: timeblock.is_finished, _timeblock_is_finished: timeblock.is_finished,
_real_start: new Date(timeblock.start), _real_start: new Date(timeblock.start),
_real_end: new Date(timeblock.end), _real_end: new Date(timeblock.end),
}); };
tasks.push(task);
} }
}); });
} }
@@ -202,22 +234,6 @@ export function useEmployeeTimeline(
return calculateTimeblockDuration(timeblock); return calculateTimeblockDuration(timeblock);
}); });
watch(
scheduledTime,
(newVal) => {
console.log('scheduledTime', newVal);
},
{ immediate: true },
);
watch(
freeTime,
(newVal) => {
console.log('freeTime', newVal);
},
{ immediate: true },
);
return { return {
// Query states // Query states
isLoading, isLoading,
@@ -23,36 +23,62 @@
<div <div
v-for="(timeblock, index) in timeblocks" v-for="(timeblock, index) in timeblocks"
:key="timeblock.id" :key="timeblock.id || `timeblock-${index}`"
class="d-flex align-center mb-3" class="timeblock-item mb-3"
> >
<v-icon <div class="d-flex align-center mb-2">
v-if="!isOffice365Task" <v-icon
class="circle-border cursor-pointer mr-2" v-if="!isOffice365Task"
icon="mdi-minus" class="circle-border cursor-pointer mr-2"
size="medium" icon="mdi-minus"
@click.stop=" size="medium"
subtask.id @click.stop="
? deleteTimeblock(timeblock) subtask.id
: deleteTimeblockWithoutId(index) ? deleteTimeblock(timeblock)
" : deleteTimeblockWithoutId(index)
/> "
<p />
class="task-badge rounded-pill" <p
:class="{ 'strike-through': timeblock.is_finished }" class="task-badge rounded-pill"
:class="{ 'strike-through': timeblock.is_finished }"
>
{{ formatTimeblockStart(timeblock) }} -
{{ formatTimeblockEnd(timeblock) }}
</p>
<v-icon
v-if="!isOffice365Task"
v-show="subtask.id"
class="circle-border cursor-pointer ml-2"
:class="{ completed: timeblock.is_finished }"
icon="mdi-check"
size="medium"
@click.stop="toggleTimeblockIsFinished(timeblock)"
/>
</div>
<!-- Timeline Visualization for existing timeblock -->
<div
v-if="employee && timeblock.date"
class="existing-timeblock-timeline ml-8"
> >
{{ formatTimeblockStart(timeblock) }} - <a-employee-timeblock-timeline
{{ formatTimeblockEnd(timeblock) }} :is-loading="getTimelineLoadingState(index)"
</p> :employee="employee"
<v-icon :scheduled-blocks="getScheduledBlocksForTimeblock(index)"
v-if="!isOffice365Task" :other-timeblocks="
v-show="subtask.id" getOtherTimeblocksForTimeblock(timeblock, index)
class="circle-border cursor-pointer ml-2" "
:class="{ completed: timeblock.is_finished }" :current-timeblock-range="getTimeblockRange(timeblock)"
icon="mdi-check" :collision="getCollisionForTimeblock(timeblock, index)"
size="medium" :collision-segments="
@click.stop="toggleTimeblockIsFinished(timeblock)" getCollisionSegmentsForTimeblock(timeblock, index)
/> "
:timeblock-duration="calculateDuration(timeblock)"
:start-hour="START_HOUR"
:end-hour="END_HOUR"
:hour-width="hourWidth"
/>
</div>
</div> </div>
</v-sheet> </v-sheet>
</v-col> </v-col>
@@ -60,12 +86,12 @@
<v-dialog <v-dialog
v-model="showModal" v-model="showModal"
max-width="750" max-width="900"
persistent persistent
:retain-focus="false" :retain-focus="false"
> >
<v-card> <v-card class="add-timeblock-modal">
<div class="d-flex justify-space-between align-center"> <div class="d-flex justify-space-between align-center pa-4">
<h1>Pridať dátum</h1> <h1>Pridať dátum</h1>
<v-icon <v-icon
icon="mdi-close" icon="mdi-close"
@@ -74,51 +100,23 @@
></v-icon> ></v-icon>
</div> </div>
<v-card-actions class="mt-10 d-flex flex-column modal-content"> <v-card-text class="pa-4">
<Z-datepicker <!-- Reuse the a-employee-timeblock component -->
v-model="form.date" <a-employee-timeblock
label="Dátum" ref="employeeTimeblockRef"
format="dd.MM.yyyy" v-model="newTimeblock"
locale="sk" :employee="employee"
select-text="Vybrať" :all-timeblocks="allTimeblocksForModal"
cancel-text="Zrušiť" :deletable="false"
placeholder="Dátum" :index="0"
:auto-apply="true"
:enable-time-picker="false"
class="mb-5 modal-input"
:class="{ error: v$.form.date.$errors.length }"
/> />
<v-text-field </v-card-text>
v-model="form.starts_at_time"
type="time"
variant="solo"
bg-color="white"
:hint="
v$.form.starts_at_time.$errors
? errorUtils.parseErrors(v$.form.starts_at_time.$errors)
: null
"
:class="{ error: v$.form.starts_at_time.$errors.length }"
class="mb-1 modal-input"
label="Začiatok"
/>
<v-text-field
v-model="form.ends_at_time"
type="time"
variant="solo"
bg-color="white"
:hint="
v$.form.ends_at_time.$errors
? errorUtils.parseErrors(v$.form.ends_at_time.$errors)
: null
"
:class="{ error: v$.form.ends_at_time.$errors.length }"
class="modal-input"
label="Koniec"
/>
</v-card-actions>
<v-btn class="button primary mt-5" @click="submit()">Potvrdiť</v-btn> <v-card-actions class="pa-4">
<v-btn class="button primary" @click="submitFromEmployeeTimeblock()">
Potvrdiť
</v-btn>
</v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-dialog v-model="visible" max-width="750" persistent :retain-focus="false"> <v-dialog v-model="visible" max-width="750" persistent :retain-focus="false">
@@ -155,17 +153,22 @@
<script> <script>
import tmUtils from '@/plugins/tm@_utils/tm.utils'; import tmUtils from '@/plugins/tm@_utils/tm.utils';
import errorUtils from '@/plugins/lib@auth/_utils/error.utils';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import ZDatepicker from '@/plugins/tm@_components/z-datepicker.vue';
import moment from 'moment'; import moment from 'moment';
import modalMixin from '@/plugins/tm@modal/_mixins/modal.mixin'; import modalMixin from '@/plugins/tm@modal/_mixins/modal.mixin';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import AEmployeeTimeblock from '../employees/a-employee-timeblock.vue';
import AEmployeeTimeblockTimeline from '../employees/a-employee-timeblock-timeline.vue';
import {
timeblockToDateRange,
detectCollision,
getCollisionSegments,
calculateTimeblockDuration,
} from '@/plugins/tm@_utils/timeblock.utils';
export default { export default {
components: { components: {
ZDatepicker, AEmployeeTimeblock,
AEmployeeTimeblockTimeline,
}, },
mixins: [modalMixin], mixins: [modalMixin],
@@ -184,37 +187,311 @@ export default {
data() { data() {
return { return {
v$: useVuelidate(),
errorUtils,
tmUtils, tmUtils,
showModal: false, showModal: false,
visible: false, visible: false,
form: { // New timeblock for modal (used by a-employee-timeblock component)
date: null, newTimeblock: {
starts_at_time: '09:00', id: null,
ends_at_time: '16:00', date: new Date(),
}, starts_at_time: null,
}; ends_at_time: null,
}, employee_id: null,
validations() {
return {
form: {
date: { required },
starts_at_time: { required },
ends_at_time: { required },
}, },
// Timeline constants
START_HOUR: 0,
END_HOUR: 24,
HOUR_WIDTH: 60,
HOUR_WIDTH_MOBILE: 50,
// Existing timeblocks timeline data
existingTimeblocksData: {}, // { [index]: { tasks: [], loading: false } }
}; };
}, },
computed: { computed: {
...mapGetters('tm', ['mastertasks']), ...mapGetters('tm', ['mastertasks', 'enrichedEmployees']),
isOffice365Task() { isOffice365Task() {
return this.subtask.type == 'office365'; return this.subtask.type == 'office365';
}, },
// Get employee data from subtask
employee() {
if (!this.subtask?.employee_id) return null;
return (
this.subtask._employee ||
this.enrichedEmployees.find(
(emp) => emp.id === this.subtask.employee_id,
)
);
},
// Hour width based on screen size
hourWidth() {
return window.innerWidth <= 768
? this.HOUR_WIDTH_MOBILE
: this.HOUR_WIDTH;
},
// All timeblocks for modal (includes existing timeblocks from this subtask)
allTimeblocksForModal() {
return this.timeblocks || [];
},
// Computed property to trigger reactivity when existingTimeblocksData changes
existingTimeblocksDataReactive() {
// This forces Vue to track changes to existingTimeblocksData
return { ...this.existingTimeblocksData };
},
},
watch: {
// Watch for timeblocks changes
timeblocks: {
handler() {
// Refetch timeline data for all timeblocks when they change
this.$nextTick(() => {
this.fetchAllTimelinesData();
});
},
deep: true,
},
// Watch for employee changes
'employee.id': {
handler(newId) {
if (newId) {
this.fetchAllTimelinesData();
}
},
},
// Watch for modal opening - set employee_id and reset to today
showModal: {
handler(isOpen) {
if (isOpen) {
this.newTimeblock = {
id: null,
date: new Date(),
starts_at_time: null,
ends_at_time: null,
employee_id: this.employee?.id || null,
};
}
},
},
},
mounted() {
// Fetch timeline data on mount
if (this.employee?.id) {
this.fetchAllTimelinesData();
}
}, },
methods: { methods: {
// Submit timeblock from employee timeblock component
async submitFromEmployeeTimeblock() {
// Validate using the employee timeblock component's validation
const isValid = await this.$refs.employeeTimeblockRef?.validate();
if (!isValid) {
return;
}
// The newTimeblock data is already reactive and updated by the component
await this.submit();
},
// Methods for existing timeblocks timeline
getTimelineLoadingState(index) {
const reactiveData = this.existingTimeblocksDataReactive;
return reactiveData[index]?.loading || false;
},
getScheduledBlocksForTimeblock(index) {
// Use the reactive computed property to ensure Vue tracks changes
const reactiveData = this.existingTimeblocksDataReactive;
const tasks = reactiveData[index]?.tasks || [];
const currentTimeblock = this.timeblocks[index];
// Filter out the current timeblock from scheduled blocks
return tasks.filter(
(task) =>
!task._timeblock_id ||
!currentTimeblock.id ||
task._timeblock_id !== currentTimeblock.id,
);
},
getOtherTimeblocksForTimeblock(timeblock, currentIndex) {
if (!timeblock.date) return [];
const selectedDate = moment(timeblock.date).format('YYYY-MM-DD');
return this.timeblocks.filter((tb, idx) => {
// Skip current timeblock
if (idx === currentIndex) return false;
if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return false;
const tbDate = moment(tb.date).format('YYYY-MM-DD');
return tbDate === selectedDate;
});
},
getTimeblockRange(timeblock) {
const { start, end } = timeblockToDateRange(timeblock);
return { start, end };
},
getCollisionForTimeblock(timeblock, index) {
const scheduledBlocks = this.getScheduledBlocksForTimeblock(index);
return detectCollision(timeblock, scheduledBlocks);
},
getCollisionSegmentsForTimeblock(timeblock, index) {
const scheduledBlocks = this.getScheduledBlocksForTimeblock(index);
return getCollisionSegments(timeblock, scheduledBlocks);
},
calculateDuration(timeblock) {
return calculateTimeblockDuration(timeblock);
},
async fetchTimelineDataForTimeblock(index) {
const timeblock = this.timeblocks[index];
if (!this.employee?.id || !timeblock.date) {
this.existingTimeblocksData = {
...this.existingTimeblocksData,
[index]: {
tasks: [],
loading: false,
},
};
return;
}
this.existingTimeblocksData = {
...this.existingTimeblocksData,
[index]: {
tasks: this.existingTimeblocksData[index]?.tasks || [],
loading: true,
},
};
try {
const dateStr = moment(timeblock.date).format('YYYY-MM-DD');
const queryParams = new URLSearchParams({
start: dateStr,
end: dateStr,
employee_id: this.employee.id.toString(),
}).toString();
const response = await this.$axios.get_auth(
`v1/subtasks?${queryParams}`,
);
const subtasks = [
...(response.subtasks || []),
...(response.blockers || []),
];
const tasks = [];
subtasks.forEach((subtask) => {
// Check if this is a blocker/adhoc/etc with direct start/end fields (no timeblocks array)
if (
!subtask.timeblocks &&
subtask.start &&
subtask.end &&
[
'blocker',
'adhoc',
'home_office',
'vacation',
'free_day',
'office365_blocker',
].includes(subtask.type)
) {
const blockerDateStr = moment(subtask.start).format('YYYY-MM-DD');
if (blockerDateStr === dateStr) {
const type = subtask.type;
tasks.push({
id: subtask.id,
start: new Date(subtask.start),
end: new Date(subtask.end),
master_task: null,
date: new Date(subtask.start),
project_code: null,
subtask: subtask,
blocker: subtask,
type,
_timeblock_id: null,
_timeblock_is_finished: subtask.is_finished || false,
_real_start: new Date(subtask.start),
_real_end: new Date(subtask.end),
});
}
} else {
// Process regular subtasks with timeblocks array
subtask.timeblocks?.forEach((tb) => {
const timeblockDateStr = moment(tb.start).format('YYYY-MM-DD');
if (timeblockDateStr === dateStr) {
let type = 'subtask';
if (subtask.type === 'office365') type = 'office365';
tasks.push({
id: subtask.id,
start: new Date(tb.start),
end: new Date(tb.end),
master_task: subtask.master_task,
date: new Date(tb.start),
project_code: subtask.project_code,
subtask: subtask,
blocker: undefined,
type,
_timeblock_id: tb.id,
_timeblock_is_finished: tb.is_finished,
_real_start: new Date(tb.start),
_real_end: new Date(tb.end),
});
}
});
}
});
this.existingTimeblocksData = {
...this.existingTimeblocksData,
[index]: {
tasks,
loading: false,
},
};
} catch (error) {
console.error('Error fetching timeline data for timeblock:', error);
this.existingTimeblocksData = {
...this.existingTimeblocksData,
[index]: {
tasks: [],
loading: false,
},
};
}
},
// Fetch timeline data for all existing timeblocks
async fetchAllTimelinesData() {
if (!this.employee?.id) return;
const promises = this.timeblocks.map((_, index) =>
this.fetchTimelineDataForTimeblock(index),
);
await Promise.all(promises);
},
formatTimeblockStart(timeblock) { formatTimeblockStart(timeblock) {
// Handle internal format with date and time fields // Handle internal format with date and time fields
if (timeblock.date && timeblock.starts_at_time) { if (timeblock.date && timeblock.starts_at_time) {
@@ -241,8 +518,6 @@ export default {
}, },
async submit() { async submit() {
if (!(await this.v$.$validate())) return;
const subtask = JSON.parse(JSON.stringify(this.subtask)); const subtask = JSON.parse(JSON.stringify(this.subtask));
if (!subtask?.timeblocks) { if (!subtask?.timeblocks) {
@@ -255,12 +530,12 @@ export default {
subtask.is_finished = 0; subtask.is_finished = 0;
} }
// Add timeblock in internal format // Add timeblock in internal format from newTimeblock
subtask.timeblocks.push({ subtask.timeblocks.push({
id: null, id: null,
date: this.form.date, date: this.newTimeblock.date,
starts_at_time: this.form.starts_at_time, starts_at_time: this.newTimeblock.starts_at_time,
ends_at_time: this.form.ends_at_time, ends_at_time: this.newTimeblock.ends_at_time,
is_finished: 0, is_finished: 0,
}); });
@@ -296,12 +571,12 @@ export default {
subtask.is_finished = 0; subtask.is_finished = 0;
} }
// Add timeblock in internal format // Add timeblock in internal format from newTimeblock
subtask.timeblocks.push({ subtask.timeblocks.push({
id: null, id: null,
date: this.form.date, date: this.newTimeblock.date,
starts_at_time: this.form.starts_at_time, starts_at_time: this.newTimeblock.starts_at_time,
ends_at_time: this.form.ends_at_time, ends_at_time: this.newTimeblock.ends_at_time,
is_finished: 0, is_finished: 0,
}); });
@@ -392,8 +667,7 @@ export default {
this._update(subtask); this._update(subtask);
}, },
deleteTimeblockWithoutId(index) { deleteTimeblockWithoutId(_index) {
console.log(index);
this.$emit('delete-timeblock'); this.$emit('delete-timeblock');
}, },
@@ -448,10 +722,12 @@ export default {
if (!deadline) return; if (!deadline) return;
const mDate = moment(this.form.date); const mDate = moment(this.newTimeblock.date);
const mDeadline = moment(deadline); const mDeadline = moment(deadline);
const [hours, minutes] = this.form.ends_at_time.split(':').map(Number); const [hours, minutes] = this.newTimeblock.ends_at_time
.split(':')
.map(Number);
mDate.hours(hours); mDate.hours(hours);
mDate.minutes(minutes); mDate.minutes(minutes);
@@ -476,20 +752,36 @@ export default {
}, },
closeModal() { closeModal() {
this.form = { // Reset newTimeblock
date: null, this.newTimeblock = {
starts_at_time: '09:00', id: null,
ends_at_time: '16:00', date: new Date(),
starts_at_time: null,
ends_at_time: null,
employee_id: this.employee?.id || null,
}; };
this.showModal = false; this.showModal = false;
this.$nextTick(() => this.v$.$reset());
}, },
}, },
}; };
</script> </script>
<style lang="sass"> <style lang="sass" scoped>
.completed .completed
background: #06dece background: #06dece
.existing-timeblock-timeline
margin-top: 8px
padding-left: 16px
// Modal specific styles
.add-timeblock-modal
::v-deep
// Hide employee select and add/remove buttons (user will handle with CSS later)
.timeblock__header__top
display: none
.actions
display: none
</style> </style>
@@ -46,6 +46,9 @@
v-for="(task, index) in scheduledBlocks" v-for="(task, index) in scheduledBlocks"
:key="`task-${index}`" :key="`task-${index}`"
class="scheduled-block" class="scheduled-block"
:class="{
'scheduled-block--blocker': ['blocker', 'adhoc', 'home_office', 'vacation', 'free_day', 'office365_blocker'].includes(task.type)
}"
:style="{ :style="{
left: `${getTimePosition(task._real_start)}px`, left: `${getTimePosition(task._real_start)}px`,
width: `${getTimeWidth(task._real_start, task._real_end)}px`, width: `${getTimeWidth(task._real_start, task._real_end)}px`,
@@ -255,6 +258,11 @@ defineExpose({
border-radius: 2px border-radius: 2px
z-index: 2 z-index: 2
&--blocker
background: rgb(176, 0, 32)
border: 1px solid rgb(156, 0, 28)
opacity: 0.7
.other-timeblock .other-timeblock
position: absolute position: absolute
top: 5px top: 5px
@@ -297,4 +305,12 @@ defineExpose({
color: white color: white
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3)
pointer-events: none pointer-events: none
.timeline-loading
height: 60px
display: flex
align-items: center
justify-content: center
font-size: 14px
opacity: 0.5
</style> </style>
@@ -240,14 +240,15 @@ const scheduledBlocks = computed(() => {
// Combine IDs that should be filtered: edited locally or deleted // Combine IDs that should be filtered: edited locally or deleted
const hiddenTimeblockIds = new Set([ const hiddenTimeblockIds = new Set([
// Timeblock IDs being edited locally // Timeblock IDs being edited locally
...props.allTimeblocks.filter(tb => tb.id).map(tb => tb.id), ...props.allTimeblocks.filter((tb) => tb.id).map((tb) => tb.id),
// Timeblock IDs that have been deleted // Timeblock IDs that have been deleted
...(props.deletedTimeblockIds || []) ...(props.deletedTimeblockIds || []),
]); ]);
// Filter out tasks whose timeblock IDs should be hidden // Filter out tasks whose timeblock IDs should be hidden
return timelineTasks.value.filter(task => return timelineTasks.value.filter(
!task._timeblock_id || !hiddenTimeblockIds.has(task._timeblock_id) (task) =>
!task._timeblock_id || !hiddenTimeblockIds.has(task._timeblock_id),
); );
}); });
@@ -271,18 +272,21 @@ const otherTimeblocksForSameDate = computed(() => {
const currentTimeblock = props.modelValue; const currentTimeblock = props.modelValue;
// Use allTimeblocks if available (includes all employees), otherwise fall back to current employee only // Use allTimeblocks if available (includes all employees), otherwise fall back to current employee only
const timeblocksToCheck = props.allTimeblocks.length > 0 const timeblocksToCheck =
? props.allTimeblocks props.allTimeblocks.length > 0
: props.allEmployeeTimeblocks; ? props.allTimeblocks
: props.allEmployeeTimeblocks;
return timeblocksToCheck.filter(tb => { return timeblocksToCheck.filter((tb) => {
// Skip current timeblock - compare by reference or by matching all key properties // Skip current timeblock - compare by reference or by matching all key properties
if (tb === currentTimeblock || if (
(tb.employee_id === currentTimeblock.employee_id && tb === currentTimeblock ||
tb.date === currentTimeblock.date && (tb.employee_id === currentTimeblock.employee_id &&
tb.starts_at_time === currentTimeblock.starts_at_time && tb.date === currentTimeblock.date &&
tb.ends_at_time === currentTimeblock.ends_at_time && tb.starts_at_time === currentTimeblock.starts_at_time &&
tb.id === currentTimeblock.id)) { tb.ends_at_time === currentTimeblock.ends_at_time &&
tb.id === currentTimeblock.id)
) {
return false; return false;
} }
@@ -353,7 +357,7 @@ watch(isLoading, (newIsLoading, oldIsLoading) => {
nextTick(() => { nextTick(() => {
setTimeout(() => { setTimeout(() => {
centerTimeline(); centerTimeline();
}, 100); // Small delay to ensure DOM is fully rendered }, 0); // Small delay to ensure DOM is fully rendered
}); });
} }
}); });