Merge branch 'main' of bitbucket.org:studiotem/jandl-taskmanager-app

This commit is contained in:
StudioTem committed 2023-05-22 15:11:32 +02:00
commit 34514e44c1
13 files changed
+201 -18

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

+4 -1
View File
@@ -43,7 +43,7 @@
height: 24px height: 24px
width: 24px width: 24px
.remove-date .circle-border
border: 1px solid rgba(0, 0, 0, 0.2) border: 1px solid rgba(0, 0, 0, 0.2)
border-radius: 50% border-radius: 50%
padding: 13px padding: 13px
@@ -53,6 +53,9 @@
.is-finished .is-finished
background: #06dece !important background: #06dece !important
.strike-through
text-decoration: line-through
.add-date .add-date
color: white color: white
border-radius: 50% border-radius: 50%
@@ -65,7 +65,7 @@
</div> </div>
</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}"> <div class="event-content" :class="{'is-blocker': event.isBlocker}">
<template v-if="event.type == 'subtask'"> <template v-if="event.type == 'subtask'">
<small v-if="event.subtask.project_code" class="project-code">{{ event.subtask.project_code }}</small> <small v-if="event.subtask.project_code" class="project-code">{{ event.subtask.project_code }}</small>
@@ -91,7 +91,8 @@
</template> </template>
<template v-else-if="event.type == 'subtask'"> <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="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> <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> </template>
@@ -222,6 +223,18 @@ export default {
this.$events.emit('show-complete-task-modal', { 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)
}
},
editTask({subtask}) { editTask({subtask}) {
this.$events.emit('show-subtask-modal', {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> <template>
<div> <div ref="page">
<A-project-filter v-if="$store.getters['tm/isInitialized']" @filterChanged="onFilterChanged" /> <A-project-filter v-if="$store.getters['tm/isInitialized']" @filterChanged="onFilterChanged" />
<W-loadable :content="filteredProjects" emptyMessage="Nenašli sa žiadne vyhovujúce projekty"> <W-loadable :content="filteredProjects" emptyMessage="Nenašli sa žiadne vyhovujúce projekty">
@@ -11,8 +11,11 @@
<script> <script>
import AProjectCalendar from './_components/a-project-calendar.vue' import AProjectCalendar from './_components/a-project-calendar.vue'
import AProjectFilter from './_components/a-project-filter.vue' import AProjectFilter from './_components/a-project-filter.vue'
import FilterMixin from '../_mixins/Filter.mixin'
export default { export default {
mixins: [FilterMixin],
components: { components: {
AProjectCalendar, AProjectCalendar,
AProjectFilter AProjectFilter
@@ -77,6 +80,8 @@ export default {
} catch (error) { } catch (error) {
this.$toast.error('Nepodarilo sa nacitat data!') this.$toast.error('Nepodarilo sa nacitat data!')
} }
this._initScrollListener()
}, },
openMasterTaskModal(mastertaskId) { openMasterTaskModal(mastertaskId) {
@@ -62,6 +62,7 @@ export default {
subtask: subtask, subtask: subtask,
type: 'subtask', type: 'subtask',
_timeblock_id: timeblock.id, _timeblock_id: timeblock.id,
_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)
} }
+13 -5
View File
@@ -1,6 +1,6 @@
<template> <template>
<div> <div ref="page">
<A-task-filter v-if="$store.getters['tm/isInitialized']" :split-days="timelineData ? splitDays : null" @filter-changed="onFilterChanged" /> <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" /> <Z-timeline v-if="timelineData" :calData="timelineData" :calConfig="config" :splits="splitDays" />
</div> </div>
</template> </template>
@@ -12,8 +12,11 @@ import { addDays, subDays } from 'date-fns'
import moment from 'moment' import moment from 'moment'
import timelineUtils from './_utils/timeline.utils' import timelineUtils from './_utils/timeline.utils'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import FilterMixin from '../_mixins/Filter.mixin'
export default { export default {
mixins: [FilterMixin],
components: { components: {
ATaskFilter, ATaskFilter,
ZTimeline ZTimeline
@@ -84,14 +87,17 @@ export default {
} else if (date.isSame(moment(), 'days')) { } else if (date.isSame(moment(), 'days')) {
// Today // Today
const elapsedMinutes = now.diff(now.clone().startOf('day'), 'minutes') const elapsedMinutes = now.diff(now.clone().startOf('day'), 'minutes')
const workStart = this.user.work_start * 60 > elapsedMinutes ? this.user.workStart : elapsedMinutes const workStart = this.user.work_start * 60 > elapsedMinutes ? this.user.work_start : elapsedMinutes
const todayCapacityMinutes = this.user.work_end * 60 - workStart const todayCapacityMinutes = (this.user.work_end - workStart) * 60
unscheduledTime = todayCapacityMinutes - schedulesCapacityBySplit[i + 1] < 0 ? 0 : todayCapacityMinutes - schedulesCapacityBySplit[i + 1] unscheduledTime = todayCapacityMinutes - schedulesCapacityBySplit[i + 1] / 60 < 0 ? 0 : todayCapacityMinutes - schedulesCapacityBySplit[i + 1]
} else { } else {
// Future // Future
unscheduledTime = dailyCapacityMinutes - schedulesCapacityBySplit[i + 1] unscheduledTime = dailyCapacityMinutes - schedulesCapacityBySplit[i + 1]
} }
if (isNaN(unscheduledTime))
unscheduledTime = dailyCapacityMinutes
days.push({ days.push({
id: i + 1, id: i + 1,
fullname: `${this.filter.employee.name} ${this.filter.employee.surname}`, fullname: `${this.filter.employee.name} ${this.filter.employee.surname}`,
@@ -173,6 +179,8 @@ export default {
} catch (error) { } catch (error) {
this.$toast.error('Nepodarilo sa nacitat data!') this.$toast.error('Nepodarilo sa nacitat data!')
} }
this._initScrollListener()
} }
} }
} }
@@ -139,6 +139,7 @@ export default {
type: 'subtask', type: 'subtask',
subtask, subtask,
_timeblock_id: timeblock.id, _timeblock_id: timeblock.id,
_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)
}) })
+5 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<div> <div ref="page">
<A-team-filter v-if="$store.getters['tm/isInitialized']" :split-days="calendarData ? splitDays : null" @filter-changed="onFilterChanged" /> <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" /> <Z-timeline v-if="calendarData" :cal-data="calendarData" :cal-config="calendarConfig" :splits="splitDays" />
</div> </div>
@@ -10,10 +10,11 @@ import ATeamFilter from './_components/a-team-filter.vue'
import ZTimeline from '@/plugins/tm@dashboard/_components/z-timeline.vue' import ZTimeline from '@/plugins/tm@dashboard/_components/z-timeline.vue'
import moment from 'moment' import moment from 'moment'
import TeamsCalendar from './_mixins/TeamsCalendar.mixin' import TeamsCalendar from './_mixins/TeamsCalendar.mixin'
import FilterMixin from '../_mixins/Filter.mixin'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
export default { export default {
mixins: [TeamsCalendar], mixins: [TeamsCalendar, FilterMixin],
components: { components: {
ATeamFilter, ATeamFilter,
@@ -80,6 +81,8 @@ export default {
} catch (error) { } catch (error) {
this.$toast.error('Nepodarilo sa nacitat data!') this.$toast.error('Nepodarilo sa nacitat data!')
} }
this._initScrollListener()
} }
} }
} }
@@ -12,7 +12,15 @@ export default {
// Added to "paginate" projects, because v-autocomplete was unable to handle so many projects at once // 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)) const projects = this.projects.filter(project => project.code.includes(this.projectCodeSearch))
return projects?.length ? projects.slice(0, 10) : this.projects.slice(0, 10) 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"> <template v-if="subtask.timesheets.length">
<div v-for="timesheet in subtask.timesheets" :key="timesheet.id" class="d-flex align-center mb-3"> <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"> <div class="d-flex align-center flex-start width-100">
<p class="task-badge rounded-pill">{{ timesheet.time }}</p> <p class="task-badge rounded-pill">{{ timesheet.time }}</p>
@@ -17,8 +17,9 @@
</div> </div>
<div v-for="timeblock in timeblocks" :key="timeblock.id" class="d-flex align-center mb-3"> <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" /> <v-icon @click.stop="deleteTimeblock(timeblock)" class="circle-border cursor-pointer mr-2" icon="mdi-minus" size="medium" />
<p class="task-badge rounded-pill">{{ tmUtils.formatDateTime(timeblock.start) }} - {{ tmUtils.formatDateTime(timeblock.end) }}</p> <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> </div>
</v-sheet> </v-sheet>
</v-col> </v-col>
@@ -113,6 +114,26 @@ export default {
this._update(subtask) 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 }) { async deleteTimeblock({ id }) {
const subtask = JSON.parse(JSON.stringify(this.subtask)) const subtask = JSON.parse(JSON.stringify(this.subtask))
subtask.timeblocks = subtask.timeblocks.filter(timeblock => timeblock.id != id) subtask.timeblocks = subtask.timeblocks.filter(timeblock => timeblock.id != id)
@@ -144,3 +165,8 @@ export default {
} }
} }
</script> </script>
<style lang="sass">
.completed
background: #06dece
</style>
+23 -3
View File
@@ -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.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> <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(), v$: useVuelidate(),
preventInputFocus: false, preventInputFocus: false,
showDatePicker: false, showDatePicker: false,
employeeError: false,
selectedNewDate: [] selectedNewDate: []
} }
}, },
@@ -113,7 +122,7 @@ export default {
subtask: { subtask: {
master_task_id: { required }, master_task_id: { required },
duration: { 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}`) const data = await this.$axios.get_auth_data(`v1/subtasks/${subtask.id}`)
this.subtask = this._enrichTask(data) this.subtask = this._enrichTask(data)
} else { } 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 this.editMode = editMode || !this.subtask.id
@@ -158,9 +167,15 @@ export default {
}, },
async save() { async save() {
if (!this.subtask.employee_id && !this.subtask.employees.length)
this.employeeError = true
if (!(await this.v$.$validate())) if (!(await this.v$.$validate()))
return return
if (!this.subtask.employee_id && !this.subtask.employees.length)
return
this.subtask.description = this.$refs['description'].getHTML() this.subtask.description = this.$refs['description'].getHTML()
try { try {
@@ -213,12 +228,17 @@ export default {
watch: { watch: {
'subtask.employee_id'(value, oldValue) { 'subtask.employee_id'(value, oldValue) {
this.employeeError = false
if (!value || !oldValue) return if (!value || !oldValue) return
this.subtask.timeblocks = [] this.subtask.timeblocks = []
this.subtask._employee = this.enrichedEmployees.find((employee) => employee.id == this.subtask.employee_id) this.subtask._employee = this.enrichedEmployees.find((employee) => employee.id == this.subtask.employee_id)
}, },
'subtask.employees'() {
this.employeeError = false
},
'subtask.master_task_id'(value) { 'subtask.master_task_id'(value) {
if (!value) return if (!value) return
this.subtask._master_task = this.mastertasks.find((mastertask) => mastertask.id == this.subtask.master_task_id) this.subtask._master_task = this.mastertasks.find((mastertask) => mastertask.id == this.subtask.master_task_id)