Files
jandl-taskmanager-app/app/src/plugins/tm@dashboard/_components/z-timeline.vue
T
2024-01-14 19:08:55 +01:00

653 lines
19 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="colWidth h-100 d-flex flex-column">
<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 d-flex align-center justify-center">
<p>{{ split.initials }}</p>
</div>
<div class="text-wrap me-4">
<p class="mb-0 mt-0 unscheduled-time-name">{{split.fullname}}</p>
<h1 class="mb-0 mt-0">{{split.unscheduledTime}} 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 }">
<div v-if="event.type == 'non-working-hours'" class="custom-event-body non-working-event colWidth h-100 d-flex flex-column justify-center">
<div class="d-flex justify-center align-center">
<v-icon icon="mdi-cancel" color="black"></v-icon>
<h3 class="mb-0 ms-2">{{ event.title }}</h3>
</div>
</div>
<div v-else-if="['subtask', 'blocker', 'adhoc', 'vacation', 'free_day'].includes(event.type)" class="custom-event-body colWidth h-100 d-flex flex-column justify-space-between" :class="{'short-event' : getEventLength(event) <= 9000000, 'mini-event' : getEventLength(event) <= 5400000, 'micro-event' : getEventLength(event) <= 3600000, 'not-approved': !event.blocker?.approved && event.isBlocker }" :style="{backgroundColor: event._timeblock_is_finished || event.subtask?.is_finished ? '#dedfe0' : event.color || event.subtask?.color}">
<div class="event-content" :class="{'is-blocker': event.isBlocker}">
<template v-if="event.type == 'subtask'">
<small v-if="event.subtask.project_code" class="project-code">{{ event.subtask.project_code }}</small>
<h3>{{ event.subtask.title ?? event.subtask.master_task?.name ?? event.subtask.master_task }}</h3>
</template>
<template v-else>
<v-tooltip :text="event.blocker?.approved ? 'Schválený' : 'Neschválený'" location="bottom">
<template v-slot:activator="{ props }">
<div v-bind="props">
<h3>{{ event.blocker?.approved ? event.blocker.name : (event.blocker.name + '*') }}</h3>
</div>
</template>
</v-tooltip>
</template>
<div class="d-flex align-center event-details event-initials-wrapper">
<div class="event-initials d-flex justify-center align-center">
<small>{{ event.initials }}</small>
</div>
<small class="event-initials-date">{{ event.date?.toLocaleDateString('sk-SK') }}</small>
</div>
<div v-if="event.type == 'subtask'" class="d-flex align-center">
<v-icon icon="mdi-skull-outline" size="small" />
<small v-if="event?.subtask.deadline" class="ml-1">{{ getDeadline(event?.subtask) }}</small>
<v-tooltip v-else text="Deadline z Mastertasku" location="bottom">
<template v-slot:activator="{ props }">
<small class="cursor-pointer ml-1" v-if="!event?.subtask?.deadline" v-bind="props">{{ getDeadline(event?.subtask) }}*</small>
</template>
</v-tooltip>
</div>
</div>
<div class="event-footer d-flex">
<template v-if="event.type == 'adhoc'">
<v-btn rounded class="event-footer-button me-2" @click.stop="editBlocker(event)"><img :src="require('@/plugins/app/_img/icons/pencil.svg')" alt=""></v-btn>
<v-btn rounded class="event-footer-button me-2" @click.stop="deleteBlocker(event)"><img :src="require('@/plugins/app/_img/icons/bin.svg')" alt=""></v-btn>
</template>
<template v-else-if="event.type == 'subtask'">
<v-btn rounded class="event-footer-button me-2" :class="{'completed': event.subtask?.is_finished}" @click.stop="completeTask(event)"><img :src="require('@/plugins/app/_img/icons/checkmark-double.svg')" alt=""></v-btn>
<v-btn rounded class="event-footer-button me-2" :class="{'completed': event._timeblock_is_finished}" @click.stop="toggleTimeblockIsFinished(event)"><img :src="require('@/plugins/app/_img/icons/checkmark.svg')" alt=""></v-btn>
<v-btn rounded class="event-footer-button me-2" @click.stop="editTask(event)"><img :src="require('@/plugins/app/_img/icons/eye.svg')" alt=""></v-btn>
<v-btn rounded class="event-footer-button me-2" @click.stop="deleteTimeBlockInTask(event)"><img :src="require('@/plugins/app/_img/icons/bin.svg')" alt=""></v-btn>
</template>
</div>
</div>
</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'
export default {
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 []
}
}
},
async created() {
console.log('splits', this.splits)
},
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
if (!this.mastertasks)
return null
const masterTask = this.mastertasks.find((mt) => mt.id === subtask.master_task_id)
return masterTask.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.round(event.startTimeMinutes / 60), Math.round(event.startTimeMinutes % 60), 0)
const end = date.setHours(Math.round(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)
}
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)
}
}
},
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')
},
getEventLength(event) {
return event.end.getTime() - event.start.getTime()
},
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)
}
},
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>
.colWidth
width: 120px
// @media (min-width: 1476px)
// width: 100% !important
.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: 20px
.day-split-header
height: fit-content
min-height: 100%
padding-right: 20px
.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
.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
width: 30px
height: 30px
background: #6200EE
border-radius: 50%
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
.custom-event-body
padding: 20px 10px
width: 100%
.event-content
&.is-blocker
*
color: #ffffff !important
.event-details .event-initials
border: 1px solid white
.event-footer
border-top: 1px solid rgba(255, 255, 255, 0.19)
&:has(.completed)
background: rgb(251, 251, 251) !important
border: 1px solid #cccccc !important
&:has(.non-working-event)
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
min-height: 350px !important
width: 100% !important
left: 0 !important
&:not(.vuecal__event--focus)
&:has(.micro-event) .custom-event-body
padding: 5px !important
&:has(.short-event) .custom-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
// display: flex
// align-items: center
// flex-wrap: nowrap
// justify-content: space-between
width: 100%
padding: 5px 10px
.event-initials-wrapper
// margin-bottom: 0
.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
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
.event-footer
flex-flow: row wrap
justify-content: center
alight-items: center
padding-top: 15px
border-top: 1px solid rgba(33, 33, 33, 0.08)
.event-footer-button
box-shadow: none !important
margin-bottom: 8px
padding: 9px !important
min-height: fit-content
height: fit-content
min-width: fit-content
width: fit-content
&.completed
background: #06dece
img
width: 18px
.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>