mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 06:20:21 +00:00
stage
This commit is contained in:
1 parent
33174b6bb1
commit
8d8c18206c
4 files changed
+447
-119
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,9 +23,10 @@
|
|||||||
|
|
||||||
<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"
|
||||||
>
|
>
|
||||||
|
<div class="d-flex align-center mb-2">
|
||||||
<v-icon
|
<v-icon
|
||||||
v-if="!isOffice365Task"
|
v-if="!isOffice365Task"
|
||||||
class="circle-border cursor-pointer mr-2"
|
class="circle-border cursor-pointer mr-2"
|
||||||
@@ -54,18 +55,43 @@
|
|||||||
@click.stop="toggleTimeblockIsFinished(timeblock)"
|
@click.stop="toggleTimeblockIsFinished(timeblock)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Timeline Visualization for existing timeblock -->
|
||||||
|
<div
|
||||||
|
v-if="employee && timeblock.date"
|
||||||
|
class="existing-timeblock-timeline ml-8"
|
||||||
|
>
|
||||||
|
<a-employee-timeblock-timeline
|
||||||
|
:is-loading="getTimelineLoadingState(index)"
|
||||||
|
:employee="employee"
|
||||||
|
:scheduled-blocks="getScheduledBlocksForTimeblock(index)"
|
||||||
|
:other-timeblocks="
|
||||||
|
getOtherTimeblocksForTimeblock(timeblock, index)
|
||||||
|
"
|
||||||
|
:current-timeblock-range="getTimeblockRange(timeblock)"
|
||||||
|
:collision="getCollisionForTimeblock(timeblock, index)"
|
||||||
|
:collision-segments="
|
||||||
|
getCollisionSegmentsForTimeblock(timeblock, index)
|
||||||
|
"
|
||||||
|
:timeblock-duration="calculateDuration(timeblock)"
|
||||||
|
:start-hour="START_HOUR"
|
||||||
|
:end-hour="END_HOUR"
|
||||||
|
:hour-width="hourWidth"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<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.length > 0
|
||||||
? props.allTimeblocks
|
? props.allTimeblocks
|
||||||
: props.allEmployeeTimeblocks;
|
: 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 === currentTimeblock ||
|
||||||
(tb.employee_id === currentTimeblock.employee_id &&
|
(tb.employee_id === currentTimeblock.employee_id &&
|
||||||
tb.date === currentTimeblock.date &&
|
tb.date === currentTimeblock.date &&
|
||||||
tb.starts_at_time === currentTimeblock.starts_at_time &&
|
tb.starts_at_time === currentTimeblock.starts_at_time &&
|
||||||
tb.ends_at_time === currentTimeblock.ends_at_time &&
|
tb.ends_at_time === currentTimeblock.ends_at_time &&
|
||||||
tb.id === currentTimeblock.id)) {
|
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user