mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 14:29:34 +00:00
Merge branch 'main' of bitbucket.org:studiotem/jandl-taskmanager-app
This commit is contained in:
commit
34514e44c1
13 files changed
+214
-31
No files matched your search
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
<path d="M2 7.82843L4.82843 10.6569L10.4853 5" stroke="black" stroke-opacity="0.87" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="7.03906" y="7.61621" width="1.09208" height="1.7726" transform="rotate(45 7.03906 7.61621)" fill="white"/>
|
||||
<path d="M6 7.82843L8.82843 10.6569L14.4853 5" stroke="black" stroke-opacity="0.87" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -43,7 +43,7 @@
|
||||
height: 24px
|
||||
width: 24px
|
||||
|
||||
.remove-date
|
||||
.circle-border
|
||||
border: 1px solid rgba(0, 0, 0, 0.2)
|
||||
border-radius: 50%
|
||||
padding: 13px
|
||||
@@ -53,6 +53,9 @@
|
||||
.is-finished
|
||||
background: #06dece !important
|
||||
|
||||
.strike-through
|
||||
text-decoration: line-through
|
||||
|
||||
.add-date
|
||||
color: white
|
||||
border-radius: 50%
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="['subtask', 'blocker', 'adhoc', 'vacation', 'free_day'].includes(event.type)" class="custom-event-body w-100 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}" :style="{backgroundColor: event.color || event.subtask.color}">
|
||||
<div v-else-if="['subtask', 'blocker', 'adhoc', 'vacation', 'free_day'].includes(event.type)" class="custom-event-body w-100 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}" :style="{backgroundColor: event._timeblock_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>
|
||||
@@ -91,7 +91,8 @@
|
||||
</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.svg')" alt=""></v-btn>
|
||||
<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="deleteTask(event)"><img :src="require('@/plugins/app/_img/icons/bin.svg')" alt=""></v-btn>
|
||||
</template>
|
||||
@@ -222,6 +223,18 @@ export default {
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
editTask({subtask}) {
|
||||
this.$events.emit('show-subtask-modal', {subtask})
|
||||
},
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
header: {
|
||||
initialized: false,
|
||||
marginTop: 0,
|
||||
calendar_previousScrollPosition: 0,
|
||||
isVisible: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
this.$refs.page?.querySelector('.vuecal__bg')?.removeEventListener('scroll', this.handleScroll)
|
||||
},
|
||||
|
||||
methods: {
|
||||
_initScrollListener(repeat = 0) {
|
||||
if (this.initialized) return
|
||||
|
||||
const calendar = this.$refs.page?.querySelector('.vuecal__bg')
|
||||
|
||||
if (!calendar)
|
||||
return setTimeout(() => {
|
||||
if (repeat < 20) this._initScrollListener(repeat + 1)
|
||||
}, 200)
|
||||
|
||||
calendar.addEventListener('scroll', this._handleScroll)
|
||||
this.initialized = true
|
||||
this.header.calendar_previousScrollPosition = calendar.scrollTop
|
||||
// if (calendar.scrollTop > 0 && this.header.isVisible) {
|
||||
// this.hideHeader()
|
||||
// }
|
||||
},
|
||||
|
||||
_handleScroll(e) {
|
||||
const header = document.querySelector('div.subheader') as HTMLElement
|
||||
|
||||
// Check if the header is initialized
|
||||
if (!this.initialized) {
|
||||
this.header.calendar_previousScrollPosition = e.target.scrollTop
|
||||
return
|
||||
}
|
||||
|
||||
if (this.header.calendar_previousScrollPosition > e.target.scrollTop) {
|
||||
// On the way up
|
||||
if (!this.header.isVisible) {
|
||||
this.showHeader()
|
||||
}
|
||||
} else if (header.offsetHeight > Math.abs(e.target.scrollTop)) {
|
||||
// On the way down
|
||||
if (this.header.isVisible) {
|
||||
this.hideHeader()
|
||||
}
|
||||
} else {
|
||||
// On the way down below filter height
|
||||
if (this.header.isVisible) {
|
||||
this.hideHeader()
|
||||
}
|
||||
}
|
||||
|
||||
this.header.calendar_previousScrollPosition = e.target.scrollTop
|
||||
},
|
||||
|
||||
hideHeader() {
|
||||
const header = document.querySelector('div.subheader') as HTMLElement
|
||||
|
||||
header.style.transition = 'margin-top 0.5s'
|
||||
header.style.marginTop = `-${header.offsetHeight}px`
|
||||
this.header.isVisible = false
|
||||
|
||||
setTimeout(() => {
|
||||
header.style.transition = ''
|
||||
}, 500)
|
||||
},
|
||||
|
||||
showHeader() {
|
||||
const header = document.querySelector('div.subheader') as HTMLElement
|
||||
|
||||
header.style.transition = 'margin-top 0.5s'
|
||||
header.style.marginTop = '0'
|
||||
this.header.isVisible = true
|
||||
|
||||
setTimeout(() => {
|
||||
header.style.transition = ''
|
||||
}, 500)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="page">
|
||||
<A-project-filter v-if="$store.getters['tm/isInitialized']" @filterChanged="onFilterChanged" />
|
||||
|
||||
<W-loadable :content="filteredProjects" emptyMessage="Nenašli sa žiadne vyhovujúce projekty">
|
||||
@@ -11,8 +11,11 @@
|
||||
<script>
|
||||
import AProjectCalendar from './_components/a-project-calendar.vue'
|
||||
import AProjectFilter from './_components/a-project-filter.vue'
|
||||
import FilterMixin from '../_mixins/Filter.mixin'
|
||||
|
||||
export default {
|
||||
mixins: [FilterMixin],
|
||||
|
||||
components: {
|
||||
AProjectCalendar,
|
||||
AProjectFilter
|
||||
@@ -77,6 +80,8 @@ export default {
|
||||
} catch (error) {
|
||||
this.$toast.error('Nepodarilo sa nacitat data!')
|
||||
}
|
||||
|
||||
this._initScrollListener()
|
||||
},
|
||||
|
||||
openMasterTaskModal(mastertaskId) {
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
subtask: subtask,
|
||||
type: 'subtask',
|
||||
_timeblock_id: timeblock.id,
|
||||
_timeblock_is_finished: timeblock.is_finished,
|
||||
_real_start: new Date(timeblock.start),
|
||||
_real_end: new Date(timeblock.end)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<A-task-filter v-if="$store.getters['tm/isInitialized']" :split-days="timelineData ? splitDays : null" @filter-changed="onFilterChanged" />
|
||||
<div ref="page">
|
||||
<A-task-filter v-if="$store.getters['tm/isInitialized']" :split-days="timelineData ? splitDays : null" @filter-changed="onFilterChanged" ref="filter" />
|
||||
<Z-timeline v-if="timelineData" :calData="timelineData" :calConfig="config" :splits="splitDays" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ATaskFilter from './_components/a-tasks-filter.vue'
|
||||
import ZTimeline from '@/plugins/tm@dashboard/_components/z-timeline.vue'
|
||||
import { addDays, subDays } from 'date-fns'
|
||||
import moment from 'moment'
|
||||
import timelineUtils from './_utils/timeline.utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import ATaskFilter from './_components/a-tasks-filter.vue'
|
||||
import ZTimeline from '@/plugins/tm@dashboard/_components/z-timeline.vue'
|
||||
import { addDays, subDays } from 'date-fns'
|
||||
import moment from 'moment'
|
||||
import timelineUtils from './_utils/timeline.utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import FilterMixin from '../_mixins/Filter.mixin'
|
||||
|
||||
export default {
|
||||
mixins: [FilterMixin],
|
||||
|
||||
components: {
|
||||
ATaskFilter,
|
||||
ZTimeline
|
||||
@@ -84,14 +87,17 @@ export default {
|
||||
} else if (date.isSame(moment(), 'days')) {
|
||||
// Today
|
||||
const elapsedMinutes = now.diff(now.clone().startOf('day'), 'minutes')
|
||||
const workStart = this.user.work_start * 60 > elapsedMinutes ? this.user.workStart : elapsedMinutes
|
||||
const todayCapacityMinutes = this.user.work_end * 60 - workStart
|
||||
unscheduledTime = todayCapacityMinutes - schedulesCapacityBySplit[i + 1] < 0 ? 0 : todayCapacityMinutes - schedulesCapacityBySplit[i + 1]
|
||||
const workStart = this.user.work_start * 60 > elapsedMinutes ? this.user.work_start : elapsedMinutes
|
||||
const todayCapacityMinutes = (this.user.work_end - workStart) * 60
|
||||
unscheduledTime = todayCapacityMinutes - schedulesCapacityBySplit[i + 1] / 60 < 0 ? 0 : todayCapacityMinutes - schedulesCapacityBySplit[i + 1]
|
||||
} else {
|
||||
// Future
|
||||
unscheduledTime = dailyCapacityMinutes - schedulesCapacityBySplit[i + 1]
|
||||
}
|
||||
|
||||
if (isNaN(unscheduledTime))
|
||||
unscheduledTime = dailyCapacityMinutes
|
||||
|
||||
days.push({
|
||||
id: i + 1,
|
||||
fullname: `${this.filter.employee.name} ${this.filter.employee.surname}`,
|
||||
@@ -173,6 +179,8 @@ export default {
|
||||
} catch (error) {
|
||||
this.$toast.error('Nepodarilo sa nacitat data!')
|
||||
}
|
||||
|
||||
this._initScrollListener()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ export default {
|
||||
type: 'subtask',
|
||||
subtask,
|
||||
_timeblock_id: timeblock.id,
|
||||
_timeblock_is_finished: timeblock.is_finished,
|
||||
_real_start: new Date(timeblock.start),
|
||||
_real_end: new Date(timeblock.end)
|
||||
})
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="page">
|
||||
<A-team-filter v-if="$store.getters['tm/isInitialized']" :split-days="calendarData ? splitDays : null" @filter-changed="onFilterChanged" />
|
||||
<Z-timeline v-if="calendarData" :cal-data="calendarData" :cal-config="calendarConfig" :splits="splitDays" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ATeamFilter from './_components/a-team-filter.vue'
|
||||
import ZTimeline from '@/plugins/tm@dashboard/_components/z-timeline.vue'
|
||||
import moment from 'moment'
|
||||
import TeamsCalendar from './_mixins/TeamsCalendar.mixin'
|
||||
import { mapGetters } from 'vuex'
|
||||
import ATeamFilter from './_components/a-team-filter.vue'
|
||||
import ZTimeline from '@/plugins/tm@dashboard/_components/z-timeline.vue'
|
||||
import moment from 'moment'
|
||||
import TeamsCalendar from './_mixins/TeamsCalendar.mixin'
|
||||
import FilterMixin from '../_mixins/Filter.mixin'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
mixins: [TeamsCalendar],
|
||||
mixins: [TeamsCalendar, FilterMixin],
|
||||
|
||||
components: {
|
||||
ATeamFilter,
|
||||
@@ -80,6 +81,8 @@ export default {
|
||||
} catch (error) {
|
||||
this.$toast.error('Nepodarilo sa nacitat data!')
|
||||
}
|
||||
|
||||
this._initScrollListener()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,16 @@ export default {
|
||||
projectsOptions() {
|
||||
// Added to "paginate" projects, because v-autocomplete was unable to handle so many projects at once
|
||||
|
||||
const projects = this.projects.filter(project => project.code.includes(this.projectCodeSearch))
|
||||
return projects?.length ? projects.slice(0, 10) : this.projects.slice(0, 10)
|
||||
const projects = this.projects.filter(project => project.code.includes(this.projectCodeSearch))
|
||||
let filteredProjects = projects?.length ? projects.slice(0, 10) : this.projects.slice(0, 10)
|
||||
|
||||
if (!this.mastertask?.project_id)
|
||||
return filteredProjects
|
||||
|
||||
const selectedProject = this.projects.find(project => project.id == this.mastertask?.project_id)
|
||||
filteredProjects = filteredProjects.filter(project => project.id != selectedProject.id)
|
||||
|
||||
return [...filteredProjects, selectedProject]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<template v-if="subtask.timesheets.length">
|
||||
<div v-for="timesheet in subtask.timesheets" :key="timesheet.id" class="d-flex align-center mb-3">
|
||||
<v-icon @click.stop="deleteTimesheet(timesheet)" class="remove-date cursor-pointer mr-2" icon="mdi-minus" size="medium" />
|
||||
<v-icon @click.stop="deleteTimesheet(timesheet)" class="circle-border cursor-pointer mr-2" icon="mdi-minus" size="medium" />
|
||||
|
||||
<div class="d-flex align-center flex-start width-100">
|
||||
<p class="task-badge rounded-pill">{{ timesheet.time }}</p>
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
</div>
|
||||
|
||||
<div v-for="timeblock in timeblocks" :key="timeblock.id" class="d-flex align-center mb-3">
|
||||
<v-icon @click.stop="deleteTimeblock(timeblock)" class="remove-date cursor-pointer mr-2" icon="mdi-minus" size="medium" />
|
||||
<p class="task-badge rounded-pill">{{ tmUtils.formatDateTime(timeblock.start) }} - {{ tmUtils.formatDateTime(timeblock.end) }}</p>
|
||||
<v-icon @click.stop="deleteTimeblock(timeblock)" class="circle-border cursor-pointer mr-2" icon="mdi-minus" size="medium" />
|
||||
<p class="task-badge rounded-pill" :class="{'strike-through': timeblock.is_finished}">{{ tmUtils.formatDateTime(timeblock.start) }} - {{ tmUtils.formatDateTime(timeblock.end) }}</p>
|
||||
<v-icon @click.stop="toggleTimeblockIsFinished(timeblock)" class="circle-border cursor-pointer ml-2" :class="{'completed': timeblock.is_finished}" icon="mdi-check" size="medium" />
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
@@ -113,6 +114,26 @@ export default {
|
||||
this._update(subtask)
|
||||
},
|
||||
|
||||
async toggleTimeblockIsFinished(originalTimeblock) {
|
||||
// TODO: Pridat odpracovany cas modal?
|
||||
const subtask = JSON.parse(JSON.stringify(this.subtask))
|
||||
const timeblock = JSON.parse(JSON.stringify(originalTimeblock))
|
||||
|
||||
console.log(subtask)
|
||||
console.log(timeblock)
|
||||
if (!timeblock.is_finished)
|
||||
this.$events.emit('show-task-add-time-modal', { subtask })
|
||||
|
||||
timeblock.is_finished = !timeblock.is_finished
|
||||
|
||||
try {
|
||||
await this.$axios.put_auth(`v1/timeblocks/${timeblock.id}`, timeblock)
|
||||
// TODO: Co mi to vrati? A updatenut
|
||||
} catch (error) {
|
||||
this.$toast.error(error.message || error)
|
||||
}
|
||||
},
|
||||
|
||||
async deleteTimeblock({ id }) {
|
||||
const subtask = JSON.parse(JSON.stringify(this.subtask))
|
||||
subtask.timeblocks = subtask.timeblocks.filter(timeblock => timeblock.id != id)
|
||||
@@ -143,4 +164,9 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.completed
|
||||
background: #06dece
|
||||
</style>
|
||||
@@ -12,7 +12,15 @@
|
||||
|
||||
<v-autocomplete v-model="subtask.master_task_id" label="Master task" :class="{error: v$.subtask.master_task_id.$errors.length}" :items="mastertasks" item-title="name" item-value="id" variant="solo" class="modal-input" clearable />
|
||||
|
||||
<v-autocomplete v-model="subtask.employee_id" label="Priradený" :class="{error: v$.subtask.employee_id.$errors.length}" :items="enrichedEmployees" item-title="_fullName" item-value="id" variant="solo" class="modal-input" clearable />
|
||||
<v-text-field v-model="subtask.name" @focus="onInputFocus" class="modal-input" label="Názov subtasku" variant="solo" :class="{ error: v$.subtask.name.$errors.length }" :hint="v$.subtask.name.$errors ? errorUtils.parseErrors(v$.subtask.name.$errors) : null"></v-text-field>
|
||||
|
||||
<template v-if="subtask.id">
|
||||
<v-autocomplete v-model="subtask.employee_id" :class="{error: employeeError}" label="Priradený" :items="enrichedEmployees" item-title="_fullName" item-value="id" variant="solo" class="modal-input" clearable />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-autocomplete v-for="(employee, index) in [...subtask.employees, null]" v-model="subtask.employees[index]" :key="`employee-${index}`" :class="{error: employeeError}" label="Priradený" :items="enrichedEmployees" item-title="_fullName" item-value="id" variant="solo" class="modal-input" clearable />
|
||||
</template>
|
||||
|
||||
<v-text-field v-model="subtask.duration" @focus="onInputFocus" class="modal-input" variant="solo" type="number" label="Časový odhad" :class="{ error: v$.subtask.duration.$errors.length }" :hint="v$.subtask.duration.$errors ? errorUtils.parseErrors(v$.subtask.duration.$errors) : null" append-inner-icon="mdi-clock-outline"></v-text-field>
|
||||
|
||||
@@ -97,6 +105,7 @@ export default {
|
||||
v$: useVuelidate(),
|
||||
preventInputFocus: false,
|
||||
showDatePicker: false,
|
||||
employeeError: false,
|
||||
selectedNewDate: []
|
||||
}
|
||||
},
|
||||
@@ -113,7 +122,7 @@ export default {
|
||||
subtask: {
|
||||
master_task_id: { required },
|
||||
duration: { required },
|
||||
employee_id: { required }
|
||||
name: { required }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -143,7 +152,7 @@ export default {
|
||||
const data = await this.$axios.get_auth_data(`v1/subtasks/${subtask.id}`)
|
||||
this.subtask = this._enrichTask(data)
|
||||
} else {
|
||||
this.subtask = { master_task: mastertask, timeblocks: [{start: tmUtils.formatDateTimeForBackend(moment()), end: tmUtils.formatDateTimeForBackend(moment())}] }
|
||||
this.subtask = { master_task: mastertask, employees: [], timeblocks: [] }
|
||||
}
|
||||
|
||||
this.editMode = editMode || !this.subtask.id
|
||||
@@ -158,9 +167,15 @@ export default {
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.subtask.employee_id && !this.subtask.employees.length)
|
||||
this.employeeError = true
|
||||
|
||||
if (!(await this.v$.$validate()))
|
||||
return
|
||||
|
||||
if (!this.subtask.employee_id && !this.subtask.employees.length)
|
||||
return
|
||||
|
||||
this.subtask.description = this.$refs['description'].getHTML()
|
||||
|
||||
try {
|
||||
@@ -213,12 +228,17 @@ export default {
|
||||
|
||||
watch: {
|
||||
'subtask.employee_id'(value, oldValue) {
|
||||
this.employeeError = false
|
||||
if (!value || !oldValue) return
|
||||
|
||||
this.subtask.timeblocks = []
|
||||
this.subtask._employee = this.enrichedEmployees.find((employee) => employee.id == this.subtask.employee_id)
|
||||
},
|
||||
|
||||
'subtask.employees'() {
|
||||
this.employeeError = false
|
||||
},
|
||||
|
||||
'subtask.master_task_id'(value) {
|
||||
if (!value) return
|
||||
this.subtask._master_task = this.mastertasks.find((mastertask) => mastertask.id == this.subtask.master_task_id)
|
||||
|
||||
Reference in new issue
Block a user