mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 14:29:34 +00:00
643 lines
17 KiB
Vue
643 lines
17 KiB
Vue
<template>
|
|
<div class="calendar-wrapper">
|
|
<Vue-cal
|
|
:hide-view-selector="true"
|
|
:hide-title-bar="true"
|
|
:disable-views="['years', 'year', 'month', 'week']"
|
|
active-view="day"
|
|
:selected-date="calConfig.date"
|
|
:onEventClick="taskClicked"
|
|
:editable-events="vueCalConfig.editing"
|
|
:events="calData.tasks"
|
|
:unscheduledTime="calData.unscheduled_times"
|
|
:overlaps-per-time-step="false"
|
|
:showTimeInCells="false"
|
|
:time-cell-height="timeCellHeight"
|
|
:snap-to-time="15"
|
|
:scroll-to-hour="calConfig.scrollHour"
|
|
:split-days="splits"
|
|
:sticky-split-labels="true"
|
|
:min-split-width="140"
|
|
@ready="onReady()"
|
|
@view-change="weekChanged"
|
|
@event-drop="eventMoved"
|
|
@event-focus="eventFocused"
|
|
>
|
|
<template v-slot:split-label="{ split }">
|
|
<div
|
|
class="h-100 d-flex flex-column"
|
|
style="width: 100%; margin-right: 10px; margin-left: 10px"
|
|
>
|
|
<span v-if="split.date" class="d-flex align-center">
|
|
<img
|
|
v-if="split.isHomeoffice"
|
|
:src="require('@/plugins/app/_img/icons/home.svg')"
|
|
class="mr-2"
|
|
/>
|
|
<p class="weekday-label">{{ getHeadingDate(split.date) }}</p>
|
|
</span>
|
|
<span v-else class="d-flex align-center">
|
|
<img
|
|
v-if="split.isHomeoffice"
|
|
:src="require('@/plugins/app/_img/icons/home.svg')"
|
|
class="mr-2"
|
|
/>
|
|
<p class="weekday-label">{{ split.name }}</p>
|
|
</span>
|
|
|
|
<span v-if="split.date">
|
|
<p class="day-name">
|
|
{{ split.date.toLocaleDateString('en-EN', { weekday: 'long' }) }}
|
|
</p>
|
|
</span>
|
|
|
|
<div
|
|
class="unscheduled-time d-flex align-center text-left"
|
|
:class="{ 'vuecal__cell-split--weekend': split.isWeekend }"
|
|
>
|
|
<div class="unscheduled-time-initials">
|
|
<p>{{ split.initials }}</p>
|
|
</div>
|
|
<div class="text-wrap me-4">
|
|
<p class="mb-0 mt-0 unscheduled-time-name">
|
|
{{ split.name }}
|
|
</p>
|
|
<p class="mb-0 mt-0 unscheduled-time-name">
|
|
{{ split.surname }}
|
|
</p>
|
|
<h1 class="mb-0 mt-0">{{ split.unscheduledTime }} h</h1>
|
|
<h1 class="mb-0 mt-0" style="font-weight: 400">
|
|
{{ split.scheduledTime }} h
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-slot:time-cell="{ hours, minutes }">
|
|
<span class="d-flex justify-center">
|
|
<span class="d-flex justify-center">
|
|
<p style="margin-top: -9px">
|
|
{{ hours }}:{{ String(minutes).padStart(2, '0') }}
|
|
</p>
|
|
<div class="time-column-line"></div>
|
|
<span class="vuecal__time-cell-line hour"></span>
|
|
</span>
|
|
|
|
<span class="vuecal__time-cell-line" style="margin-top: 25px"></span>
|
|
<span class="vuecal__time-cell-line" style="margin-top: 50px"></span>
|
|
<span class="vuecal__time-cell-line" style="margin-top: 75px"></span>
|
|
</span>
|
|
</template>
|
|
|
|
<template v-slot:event="{ event }">
|
|
<A-timeline-event
|
|
:event="event"
|
|
@edit-blocker="editBlocker"
|
|
@delete-blocker="deleteBlocker"
|
|
@complete-task="completeTask"
|
|
@toggle-timeblock-is-finished="toggleTimeblockIsFinished"
|
|
@edit-task="editTask"
|
|
@delete-timeblock-in-task="deleteTimeBlockInTask"
|
|
/>
|
|
</template>
|
|
</Vue-cal>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { addDays } from 'date-fns';
|
|
import tmUtils from '@/plugins/tm@_utils/tm.utils';
|
|
import { mapGetters } from 'vuex';
|
|
import moment from 'moment';
|
|
import ATimelineEvent from './a-timeline-event.vue';
|
|
|
|
export default {
|
|
components: {
|
|
ATimelineEvent,
|
|
},
|
|
props: {
|
|
calData: {
|
|
type: Object,
|
|
required: true,
|
|
default() {
|
|
return {};
|
|
},
|
|
},
|
|
calConfig: {
|
|
type: Object,
|
|
required: true,
|
|
default() {
|
|
return { date: new Date(), scrollHour: 7 };
|
|
},
|
|
},
|
|
splits: {
|
|
type: Array,
|
|
required: false,
|
|
default() {
|
|
return [];
|
|
},
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
vueCalConfig: {
|
|
editing: {
|
|
title: false,
|
|
drag: true,
|
|
resize: false,
|
|
delete: false,
|
|
create: false,
|
|
},
|
|
},
|
|
focusedEventId: null,
|
|
timeCellHeight: 100,
|
|
tmUtils,
|
|
visible: false,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters('tm', ['mastertasks']),
|
|
},
|
|
|
|
methods: {
|
|
isAfterDeadline(event) {
|
|
const deadline = this.getDeadline(event.subtask);
|
|
|
|
if (!deadline) return;
|
|
|
|
const mDate = moment(event.date);
|
|
const mDeadline = moment(deadline);
|
|
const hours = event.end.getHours();
|
|
const minutes = event.end.getMinutes();
|
|
|
|
mDate.hours(hours);
|
|
mDate.minutes(minutes);
|
|
|
|
return mDate.isAfter(mDeadline);
|
|
},
|
|
|
|
getDeadline(subtask) {
|
|
if (subtask?.deadline) return subtask.deadline;
|
|
|
|
return subtask.master_task_deadline;
|
|
},
|
|
|
|
weekChanged() {
|
|
const scrollContainer = this.$el.getElementsByClassName(
|
|
'vuecal__scrollable-body',
|
|
)[0];
|
|
scrollContainer.scrollLeft = 0;
|
|
},
|
|
|
|
async eventMoved({ event, newSplit }) {
|
|
const date = addDays(this.calConfig.date, newSplit - 1);
|
|
const start = date.setHours(
|
|
Math.floor(event.startTimeMinutes / 60),
|
|
Math.round(event.startTimeMinutes % 60),
|
|
0,
|
|
);
|
|
const end = date.setHours(
|
|
Math.floor(event.endTimeMinutes / 60),
|
|
Math.round(event.endTimeMinutes % 60),
|
|
0,
|
|
);
|
|
const split = this.splits.find((split) => split.id == newSplit);
|
|
|
|
if (this.$route.name == 'Dashboard - Tasks') {
|
|
const editedSubtask = {
|
|
...event.subtask,
|
|
timeblocks: [
|
|
...event.subtask.timeblocks,
|
|
{
|
|
id: event._timeblock_id,
|
|
is_finished: 0,
|
|
start: tmUtils.formatDateTimeForBackend(start),
|
|
end: tmUtils.formatDateTimeForBackend(end),
|
|
},
|
|
],
|
|
timesheets: event.subtask.timesheets.map((timesheet) => {
|
|
delete timesheet.employee_name;
|
|
return timesheet;
|
|
}),
|
|
};
|
|
|
|
try {
|
|
const { subtask } = await this.$axios.put_auth(
|
|
`v1/subtasks/${editedSubtask.id}`,
|
|
editedSubtask,
|
|
);
|
|
this.$events.emit('on-subtask-edited', subtask);
|
|
|
|
if (subtask.employee_id == this.$store.getters['auth/user'].id)
|
|
this.$events.emit('show-task-add-time-modal', { subtask });
|
|
} catch (error) {
|
|
this.$toast.error(error.message || error);
|
|
|
|
if (process.env.NODE_ENV === 'development')
|
|
console.log('[z-timeline.vue] Error: ', error);
|
|
}
|
|
|
|
const isAfterDeadline = this.isAfterDeadline(event);
|
|
|
|
if (isAfterDeadline)
|
|
this.$toast.error('Timeblock, ktorý ste presunuli je po deadline !');
|
|
} else if (this.$route.name == 'Dashboard - Teams') {
|
|
const editedSubtask = {
|
|
...event.subtask,
|
|
employee_id: split.employee_id,
|
|
timeblocks: [
|
|
...event.subtask.timeblocks,
|
|
{
|
|
id: event._timeblock_id,
|
|
is_finished: 0,
|
|
start: tmUtils.formatDateTimeForBackend(event.start),
|
|
end: tmUtils.formatDateTimeForBackend(event.end),
|
|
},
|
|
],
|
|
timesheets: event.subtask.timesheets.map((timesheet) => {
|
|
delete timesheet.employee_name;
|
|
return timesheet;
|
|
}),
|
|
};
|
|
try {
|
|
const { subtask } = await this.$axios.put_auth(
|
|
`v1/subtasks/${editedSubtask.id}`,
|
|
editedSubtask,
|
|
);
|
|
this.$events.emit('on-subtask-edited', subtask);
|
|
} catch (error) {
|
|
this.$toast.error(error.message || error);
|
|
|
|
if (process.env.NODE_ENV === 'development')
|
|
console.log('[z-timeline.vue] Error: ', error);
|
|
}
|
|
}
|
|
},
|
|
|
|
onReady() {
|
|
this.correctMargins();
|
|
},
|
|
|
|
correctMargins() {
|
|
if (!this.$el.getElementsByClassName('vuecal__split-days-headers').length)
|
|
return;
|
|
|
|
const heightOfDayHeader = this.$el.getElementsByClassName(
|
|
'vuecal__split-days-headers',
|
|
)[0].clientHeight;
|
|
this.$el.getElementsByClassName(
|
|
'vuecal__time-column',
|
|
)[0].style.marginTop = `${heightOfDayHeader}px`;
|
|
this.$el.getElementsByClassName(
|
|
'vuecal__cells-body',
|
|
)[0].style.marginTop = `${heightOfDayHeader}px`;
|
|
},
|
|
|
|
getHeadingDate(value) {
|
|
const date = new Date(value);
|
|
|
|
const today = new Date();
|
|
const yesterday = new Date(new Date().setDate(today.getDate() - 1));
|
|
const tomorrow = new Date(new Date().setDate(today.getDate() + 1));
|
|
|
|
if (
|
|
today.toLocaleDateString('sk-SK') === date.toLocaleDateString('sk-SK')
|
|
)
|
|
return 'Dnes';
|
|
if (
|
|
yesterday.toLocaleDateString('sk-SK') ===
|
|
date.toLocaleDateString('sk-SK')
|
|
)
|
|
return 'Včera';
|
|
if (
|
|
tomorrow.toLocaleDateString('sk-SK') ===
|
|
date.toLocaleDateString('sk-SK')
|
|
)
|
|
return 'Zajtra';
|
|
|
|
return date.toLocaleDateString('sk-SK');
|
|
},
|
|
|
|
taskClicked(subtask) {
|
|
// console.log('task clicked')
|
|
},
|
|
|
|
eventFocused(event) {
|
|
this.focusedEventId = event.task_id;
|
|
// console.log(`Focused: ${event.task_id}`)
|
|
},
|
|
|
|
completeTask({ subtask }) {
|
|
this.$events.emit('show-complete-task-modal', { subtask });
|
|
},
|
|
|
|
// async toggleTimeblockIsFinished(event) {
|
|
// if (!event.timeblock.is_finished)
|
|
// this.$events.emit('show-task-add-time-modal', { subtask: event.subtask })
|
|
|
|
// try {
|
|
// await this.$axios.put_auth(`v1/timeblocks/${event._timeblock_id}`, {is_finished: !event.timeblock.is_finished})
|
|
// // TODO: Co mi to vrati? A updatenut
|
|
// } catch (error) {
|
|
// this.$toast.error(error.message || error)
|
|
// }
|
|
// },
|
|
|
|
async toggleTimeblockIsFinished(event) {
|
|
const subtask = event.subtask;
|
|
const timeblock = subtask.timeblocks.find(
|
|
(originalTimeblock) => originalTimeblock.id == event._timeblock_id,
|
|
);
|
|
|
|
timeblock.is_finished = timeblock?.is_finished ? 0 : 1;
|
|
|
|
try {
|
|
const { data: newTimeblock } = await this.$axios.put_auth(
|
|
`v1/timeblocks/${timeblock.id}`,
|
|
timeblock,
|
|
);
|
|
|
|
subtask.timeblocks = subtask.timeblocks.map((originalTimeblock) => {
|
|
if (originalTimeblock.id == newTimeblock.id) return newTimeblock;
|
|
|
|
return originalTimeblock;
|
|
});
|
|
|
|
this.$events.emit('on-subtask-edited', subtask);
|
|
|
|
if (timeblock.is_finished)
|
|
this.$events.emit('show-task-add-time-modal', { subtask });
|
|
} catch (error) {
|
|
this.$toast.error(error.message || error);
|
|
|
|
if (process.env.NODE_ENV === 'development')
|
|
console.log('[z-timeline.vue] Error: ', error);
|
|
}
|
|
},
|
|
|
|
editTask({ subtask }) {
|
|
this.$events.emit('show-subtask-modal', { subtask });
|
|
},
|
|
|
|
deleteTimeBlockInTask({ subtask }) {
|
|
this.$events.emit('show-delete-timeblock-modal', { subtask });
|
|
},
|
|
|
|
editBlocker({ blocker }) {
|
|
this.$events.emit('show-blocker-modal', { blocker });
|
|
},
|
|
|
|
deleteBlocker({ blocker }) {
|
|
this.$events.emit('show-delete-blocker-modal', { blocker });
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="sass" scoped>
|
|
.vuecal
|
|
box-shadow: none !important
|
|
|
|
.calendar-wrapper
|
|
width: 100%
|
|
height: 100%
|
|
// padding: 0 20px 0 0
|
|
|
|
&:deep()
|
|
.vuecal__cells
|
|
-ms-overflow-style: none
|
|
scrollbar-width: none
|
|
&::-webkit-scrollbar
|
|
display: none
|
|
|
|
border-left: 1px solid rgba(0, 0, 0, .1)
|
|
padding-left: 20px
|
|
|
|
.vuecal__flex
|
|
flex-wrap: nowrap !important
|
|
|
|
|
|
// TIME COLUMN
|
|
.vuecal__time-column
|
|
width: 80px
|
|
box-shadow: none !important
|
|
|
|
.time-column-line
|
|
width: 20px
|
|
height: 0
|
|
border: 1px solid rgba(33, 33, 33, 0.08)
|
|
margin-left: 10px
|
|
|
|
@media (max-width: 450px)
|
|
.vuecal__time-column
|
|
width: 70px
|
|
|
|
.time-column-line
|
|
width: 10px
|
|
|
|
|
|
// HEADING
|
|
// .vuecal__weekdays-headings,
|
|
.vuecal__cell:before
|
|
border: none
|
|
|
|
.vuecal__scrollable-body
|
|
position: relative
|
|
|
|
// .vuecal__weekdays-headings
|
|
.vuecal__split-days-headers
|
|
position: absolute
|
|
top: 0
|
|
left: 20px
|
|
z-index: 999
|
|
background: white
|
|
padding-bottom: 15px
|
|
width: calc( 100% - 20px )
|
|
|
|
.vuecal__cell-split
|
|
margin-right: 10px
|
|
margin-left: 10px
|
|
|
|
.day-split-header
|
|
height: fit-content
|
|
min-height: 100%
|
|
// margin-left: 10px
|
|
|
|
.weekday-label
|
|
justify-content: start !important
|
|
font-size: 18px
|
|
font-weight: 700
|
|
|
|
.day-name
|
|
text-align: left
|
|
font-size: 17px
|
|
font-weight: 500
|
|
text-transform: capitalize
|
|
|
|
.unscheduled-time
|
|
background: rgba(0, 0, 0, 0.04)
|
|
border-radius: 24px
|
|
padding: 10px 10px
|
|
margin-top: 10px
|
|
container-type: inline-size
|
|
width: 100%
|
|
|
|
@container (max-width: 120px)
|
|
.unscheduled-time-name
|
|
width: 60px
|
|
overflow: hidden
|
|
text-overflow: ellipsis
|
|
white-space: nowrap
|
|
|
|
.text-wrap
|
|
width: min-content
|
|
margin-left: 12px
|
|
|
|
h1
|
|
font-size: 14px
|
|
|
|
p
|
|
font-weight: 400
|
|
font-size: 15px
|
|
line-height: normal
|
|
|
|
.unscheduled-time-initials
|
|
display: flex
|
|
align-items: center
|
|
justify-content: center
|
|
width: 30px !important
|
|
height: 30px !important
|
|
background: #6200EE
|
|
border-radius: 50%
|
|
flex-shrink: 0
|
|
|
|
p
|
|
font-weight: 400
|
|
font-size: 15px
|
|
color: white
|
|
|
|
|
|
// BODY
|
|
.vuecal__body
|
|
-ms-overflow-style: none
|
|
scrollbar-width: none
|
|
&::-webkit-scrollbar
|
|
display: none
|
|
|
|
.vuecal__now-line
|
|
display: none !important
|
|
|
|
// .vuecal__cell
|
|
// padding: 10px 20px 10px 0px
|
|
|
|
.vuecal:not(.vuecal--day-view) .vuecal__cell--selected:not(.vuecal__cell--today) ,
|
|
.vuecal__cell--current:not(.vuecal__cell--today),
|
|
.vuecal__cell--today,
|
|
.vuecal__cell--highlighted,
|
|
.vuecal__cell--selected
|
|
// background-color: white
|
|
border-radius: 16px
|
|
|
|
.vuecal__cell-split--weekend
|
|
// background-color: #F2E7FE !important
|
|
background-color: rgba(255, 124, 124, .3) !important
|
|
border-radius: 16px
|
|
|
|
// EVENT
|
|
|
|
.vuecal__event
|
|
border-radius: 16px
|
|
min-height: 70px !important
|
|
text-align: left
|
|
background: rgba(248, 248, 248, 1)
|
|
transition: min-height .4s, height .4s, box-shadow .4s, width .4s, left .4s
|
|
// border: 1px solid rgba(33, 33, 33, 0.1)
|
|
|
|
&:has(.completed)
|
|
background: rgb(251, 251, 251) !important
|
|
border: 1px solid #cccccc !important
|
|
|
|
&:has(.event__body--non-working-hours)
|
|
background: repeating-linear-gradient(135deg, rgba(215, 215, 215, 1), rgba(215, 215, 215, 1) 1px, rgba(224, 224, 224, 1) 1px, rgba(224, 224, 224, 1) 20px)
|
|
border: 1px solid rgba(215, 215, 215, 1)
|
|
|
|
&.vuecal__event--focus
|
|
z-index: 100 !important
|
|
height: auto !important // Fix to work with safari as well
|
|
max-height: fit-content !important // Fix to work with safari as well
|
|
width: 100% !important
|
|
left: 0 !important
|
|
|
|
&:not(.vuecal__event--focus)
|
|
&:has(.micro-event) .event__body
|
|
padding: 5px !important
|
|
|
|
&:has(.short-event) .event__body:not(.micro-event)
|
|
padding: 10px 5px !important
|
|
.short-event
|
|
.project-code
|
|
display: none !important
|
|
|
|
h3
|
|
font-size: 16px !important
|
|
|
|
.mini-event
|
|
.event-content
|
|
width: 100%
|
|
padding: 5px 10px
|
|
|
|
.event-initials-wrapper
|
|
.event-initials
|
|
margin-right: 0
|
|
|
|
h3
|
|
font-size: 14px !important
|
|
text-overflow: ellipsis
|
|
width: calc( 100% - 35px )
|
|
white-space: nowrap
|
|
overflow: hidden
|
|
margin: 0
|
|
|
|
.event-initials-date
|
|
display: none !important
|
|
|
|
.micro-event
|
|
|
|
.event__footer-wrapper
|
|
display: none !important
|
|
|
|
.project-code
|
|
margin: 0 0 20px
|
|
display: block
|
|
|
|
h3
|
|
font-size: 15px
|
|
font-weight: 400
|
|
color: Black
|
|
margin: 0 0 10px
|
|
max-height: 100%
|
|
|
|
small
|
|
color: black
|
|
|
|
.event-initials-wrapper
|
|
margin-bottom: 20px
|
|
|
|
.event-initials
|
|
height: 30px
|
|
width: 30px
|
|
border: 1px solid black
|
|
border-radius: 50%
|
|
padding: 5px
|
|
margin-right: 7px
|
|
|
|
small
|
|
font-size: 11px
|
|
|
|
.not-approved
|
|
background: repeating-linear-gradient(135deg, rgb(215, 215, 215), rgb(215, 215, 215) 1px, rgb(224, 224, 224) 1px, rgb(224, 224, 224) 20px) !important
|
|
</style>
|