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

This commit is contained in:
StudioTem committed 2023-04-12 08:50:24 +02:00
commit bbc476f5cd
15 files changed
+305 -247

No files matched your search

@@ -7,8 +7,9 @@
<td>{{ mastertask.name }}</td>
<td class="task-action">
<div class="d-flex justify-start">
<img class="pencil-icon" :src="require('@/plugins/app/_img/icons/pencil.svg')" alt="pencil-icon" @click.stop="openEditMasterTaskModal(mastertask)">
<img class="trash-icon" :src="require('@/plugins/app/_img/icons/bin.svg')" alt="trash-icon" @click.stop="openDeleteMasterTaskModal(mastertask)">
<img class="pencil-icon" :src="require('@/plugins/app/_img/icons/pencil.svg')" alt="pencil-icon" @click.stop="openEditMasterTaskModal(mastertask, true)">
<img class="pencil-icon ml-2" :src="require('@/plugins/app/_img/icons/eye.svg')" alt="eye-icon" @click.stop="openEditMasterTaskModal(mastertask, false)">
<img class="trash-icon ml-2" :src="require('@/plugins/app/_img/icons/bin.svg')" alt="trash-icon" @click.stop="openDeleteMasterTaskModal(mastertask)">
</div>
</td>
</tr>
@@ -36,8 +37,8 @@ export default {
this.$events.emit('show-delete-master-task-modal', { mastertask })
},
openEditMasterTaskModal(mastertask) {
this.$events.emit('show-mastertask-modal', { mastertask, editMode: false })
openEditMasterTaskModal(mastertask, editMode) {
this.$events.emit('show-mastertask-modal', { mastertask, editMode })
},
}
}
@@ -15,8 +15,9 @@
<div class="d-flex justify-start">
<img v-if="subtask.is_finished" class="pencil-icon is-finished" :src="require('@/plugins/app/_img/icons/checkmark.svg')" alt="checkmark-icon" @click.stop="clickCompleteTask(subtask)">
<img v-if="!subtask.is_finished" class="pencil-icon" :src="require('@/plugins/app/_img/icons/checkmark.svg')" alt="checkmark-icon" @click.stop="clickCompleteTask(subtask)">
<img class="pencil-icon ml-2" :src="require('@/plugins/app/_img/icons/eye.svg')" alt="pencil-icon" @click.stop="openTaskModal(subtask)">
<img class="trash-icon" :src="require('@/plugins/app/_img/icons/bin.svg')" alt="trash-icon" @click.stop="openDeleteTaskModal(subtask)">
<img class="pencil-icon ml-2" :src="require('@/plugins/app/_img/icons/pencil.svg')" alt="pencil-icon" @click.stop="openTaskModal(subtask, true)">
<img class="pencil-icon ml-2" :src="require('@/plugins/app/_img/icons/eye.svg')" alt="eye-icon" @click.stop="openTaskModal(subtask, false)">
<img class="trash-icon ml-2" :src="require('@/plugins/app/_img/icons/bin.svg')" alt="trash-icon" @click.stop="openDeleteTaskModal(subtask)">
</div>
</td>
</tr>
@@ -61,8 +62,8 @@ export default {
this.$events.emit('show-delete-task-modal', { subtask })
},
openTaskModal(subtask) {
this.$events.emit('show-subtask-modal', { subtask, editMode: false })
openTaskModal(subtask, editMode) {
this.$events.emit('show-subtask-modal', { subtask, editMode })
},
async clickCompleteTask(subtask) {
+44 -13
View File
@@ -1,9 +1,14 @@
<template>
<form>
<A-search-result-mastertasks v-if="searchResults?.mastertasks" :mastertasks="searchResults.mastertasks"/>
<A-search-result-subtasks v-if="searchResults?.subtasks" :subtasks="searchResults.subtasks" />
<A-search-result-team v-if="searchResults?.teams" :teams="searchResults.teams" />
</form>
<div>
<p v-if="!this.searchResults" class="px-3 py-3">{{ this.search?.length ? $t('Searching...') : $t('Type something') }}</p>
<p v-else-if="this.searchResults.error" class="px-3">{{ this.searchResults.error }}</p>
<div v-else>
<A-search-result-mastertasks :mastertasks="searchResults.mastertasks"/>
<A-search-result-subtasks :subtasks="searchResults.subtasks" />
<A-search-result-team :teams="searchResults.teams" />
</div>
</div>
</template>
<script>
@@ -21,33 +26,59 @@ export default {
data() {
return {
searchResults: {},
searchResults: null,
search: ''
}
},
created() {
this.$events.on('onSearch', (search) => {
if (!search?.length)
return
this.search = search
this.onSearch(search)
})
},
watch: {
search(newVal) {
this.updateSearchResults(newVal)
}
mounted() {
this.$events.on('on-subtask-edited', () => {
this.onSearch(this.search, false)
})
this.$events.on('on-subtask-deleted', () => {
this.onSearch(this.search, false)
})
this.$events.on('on-mastertask-edited', () => {
this.onSearch(this.search, false)
})
this.$events.on('on-mastertask-deleted', () => {
this.onSearch(this.search, false)
})
},
unmounted() {
this.$events.off('on-subtask-edited')
this.$events.off('on-subtask-deleted')
this.$events.off('on-mastertask-edited')
this.$events.off('on-mastertask-deleted')
},
methods: {
updateSearchResults: debounce(async function (search) {
onSearch: debounce(async function (search, reset = true) {
try {
if (reset)
this.searchResults = null
const { mastertasks, subtasks, teams} = await this.$axios.get_auth(`v1/search?q=${search}`)
this.searchResults = {mastertasks: mastertasks.data, subtasks: subtasks.data, teams: teams.data}
console.log(this.searchResults)
} catch (error) {
this.$toast.error(error)
this.searchResults = {error}
}
}, 500)
}, 300)
}
}
</script>
@@ -26,10 +26,11 @@
<template v-slot:split-label="{ split }">
<div class="w-100 h-100 d-flex flex-column">
<span v-if="split.date" class="d-flex align-center">
<img v-if="getHeadingDate(split.date) == 'Dnes'" :src="require('@/plugins/app/_img/icons/home.svg')" class="mr-2">
<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>
@@ -64,15 +65,15 @@
</div>
</div>
<div v-else-if="event.blocker?.type != 'home_office'" 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 class="event-content" :class="event.type || 'subtask'">
<template v-if="event.type == 'blocker'">
<h3>{{ event.blocker.name }}</h3>
<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 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.master_task?.name || event.subtask.master_task }}</h3>
</template>
<template v-else>
<small v-if="event.subtask.project_code" class="project-code">{{ event.subtask.project_code }}</small>
<h3>{{ event.subtask.master_task?.name || event.subtask.master_task }}</h3>
<h3>{{ event.blocker.name }}</h3>
</template>
<div class="d-flex align-center event-details event-initials-wrapper">
@@ -84,12 +85,12 @@
</div>
<div class="event-footer d-flex">
<template v-if="event.type == 'blocker'">
<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>
<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" @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>
@@ -209,11 +210,7 @@ export default {
},
taskClicked(subtask) {
if (subtask.type == 'non-working-hours') return
// console.log("task", task)
if (subtask.end.getTime() - subtask.start.getTime() <= 3600000) {
// console.log("ShortTask")
}
// console.log('task clicked')
},
eventFocused(event) {
@@ -385,7 +382,7 @@ export default {
padding: 20px 10px
.event-content
&.blocker
&.is-blocker
*
color: #ffffff !important
@@ -3,10 +3,15 @@ import moment from 'moment'
export default {
generateTimelineTasks(subtasks, date, employee) {
return [
...this.generateTimelineEvents(subtasks, employee, date),
...this.generateTimelineNonWorkingHours(date, employee)
]
const { tasks, blockers, homeoffices } = this.generateTimelineEvents(subtasks, employee, date)
const { nonWorkingHours } = this.generateTimelineNonWorkingHours(date, employee)
return {
tasks,
blockers,
nonWorkingHours,
homeoffices
}
},
getUnscheduledTime(subtasks) {
@@ -21,19 +26,24 @@ export default {
},
generateTimelineEvents(subtasks, employee, date) {
let events = []
let blockers = []
const tasks = []
subtasks.forEach(subtask => {
if (subtask.type == 'blocker') {
events = [...events, ...this._generateEventsFromBlocker(subtask, date)]
if (['blocker', 'adhoc', 'home_office', 'vacation', 'free_day'].includes(subtask.type)) {
blockers = [...blockers, ...this._generateEventsFromBlocker(subtask, date)]
} else {
subtask.timeblocks?.forEach((timeblock) => {
events.push(this._composeTimelineEvent(subtask, employee, date, timeblock))
tasks.push(this._composeTimelineEvent(subtask, employee, date, timeblock))
})
}
})
return events
return {
tasks,
blockers: blockers.filter(blocker => blocker.type != 'home_office'),
homeoffices: blockers.filter(blocker => blocker.type == 'home_office')
}
},
_composeTimelineEvent(subtask, employee, date, timeblock) {
@@ -50,7 +60,10 @@ export default {
split: this._getSplitFromDate(date, timeblock.start),
project_code: subtask.project_code,
subtask: subtask,
_timeblock_id: timeblock.id
type: 'subtask',
_timeblock_id: timeblock.id,
_real_start: new Date(timeblock.start),
_real_end: new Date(timeblock.end)
}
},
@@ -58,8 +71,7 @@ export default {
const start = moment(blocker.start)
const end = moment(blocker.end)
const dates = start == end ? [moment(start).format('YYYY-MM-DD')] : this._getDatesInRange(start, end)
const dates = start.isSame(end, 'date') ? [moment(start).format('YYYY-MM-DD')] : this._getDatesInRange(start, end)
const blockers = dates.map(d => {
const diffInDays = differenceInCalendarDays(date, new Date(d))
const dStart = new Date(moment(d).set({hours: start.hours(), minutes: start.minutes()}).format('YYYY-MM-DD HH:mm'))
@@ -71,10 +83,13 @@ export default {
start: addDays(dStart, diffInDays),
end: addDays(dEnd, diffInDays),
date: new Date(d),
type: 'blocker',
type: blocker.type,
isBlocker: true,
initials: `${blocker.employee?.name[0]}${blocker.employee?.surname[0]}`,
color: '#B00020',
draggable: false
draggable: false,
_real_start: dStart,
_real_end: dEnd
}
})
@@ -117,7 +132,7 @@ export default {
})
}
return events
return {nonWorkingHours: events}
},
_getSplitFromDate(date, start) {
@@ -125,8 +140,12 @@ export default {
},
_getDifferenceInHours(date1, date2) {
let diff =(date1.getTime() - date2.getTime()) / 1000;
diff /= (60 * 60)
return this._getDifferenceInMinutes(date1, date2) / 60
},
_getDifferenceInMinutes(date1, date2) {
let diff = (date1.getTime() - date2.getTime()) / 1000
diff /= 60
return Math.abs(Math.round(diff))
},
+60 -9
View File
@@ -37,29 +37,77 @@ export default {
splitDays() {
const days = []
const workingHours = {}
const homeoffice = {}
const schedulesCapacityBySplit = {}
const now = moment()
const dailyCapacityMinutes = this.user.work_end - this.user.work_start
this.timelineData.tasks.forEach(task => {
if (!workingHours[task.split])
workingHours[task.split] = 0
if (task.type == 'non-working-hours' || task.subtask?.is_finished || task.blocker?.is_finished)
return
workingHours[task.split] += timelineUtils._getDifferenceInHours(task.end, task.start)
if (!schedulesCapacityBySplit[task.split])
schedulesCapacityBySplit[task.split] = 0
if (now.isAfter(moment(task._real_end))) {
// Ignore elapsed task
return
}
else if (now.isAfter(moment(task._real_start) && now.isBefore(moment(task._real_end)))) {
// Task in progress
schedulesCapacityBySplit[task.split] += timelineUtils._getDifferenceInMinutes(task._real_end, new Date())
}
else {
// Future task
schedulesCapacityBySplit[task.split] += timelineUtils._getDifferenceInMinutes(task._real_end, task._real_start)
}
})
schedulesCapacityBySplit[1] = 0 // Set YESTERDAY value to 0
this.timelineData.homeoffices.forEach(task => {
homeoffice[task.split] = true
})
for (let i = 0; i < 7; i++) {
if (!schedulesCapacityBySplit[i + 1])
schedulesCapacityBySplit[i + 1] = 0
let unscheduledTime
if (i == 0) {
// Yesterday
unscheduledTime = 0
} else if (i == 1) {
// Today
const elapsedMinutes = now.diff(now.clone().startOf('day'), 'minutes')
const workStart = this.user.work_start > elapsedMinutes ? this.user.workStart : elapsedMinutes
const todayCapacityMinutes = this.user.work_end - workStart
unscheduledTime = todayCapacityMinutes - schedulesCapacityBySplit[i + 1] < 0 ? 0 : todayCapacityMinutes - schedulesCapacityBySplit[i + 1]
} else {
// Future
unscheduledTime = dailyCapacityMinutes - schedulesCapacityBySplit[i + 1]
}
console.log(unscheduledTime)
days.push({
id: i + 1,
fullname: `${this.filter.employee.name} ${this.filter.employee.surname}`,
initials: this.filter.employee.name.charAt(0).toUpperCase() + this.filter.employee.surname.charAt(0).toUpperCase(),
unscheduledTime: workingHours[i + 1],
unscheduledTime: (unscheduledTime / 60).toFixed(1),
date: addDays(this.filter.start, i),
isHomeoffice: homeoffice[i],
isWeekend: timelineUtils.isWeekend(addDays(this.filter.start, i))
})
}
console.log(days)
return days
},
@@ -74,11 +122,14 @@ export default {
if (!this.subtasks || !this.filter)
return null
const { tasks, blockers, nonWorkingHours, homeoffices } = timelineUtils.generateTimelineTasks(this.subtasks, new Date(this.filter.start), this.filter.employee)
return {
name: this.filter.employee.name,
surname: this.filter.employee.surname,
unscheduledTime: this.unscheduledTime,
tasks: timelineUtils.generateTimelineTasks(this.subtasks, new Date(this.filter.start), this.filter.employee)
tasks: [...tasks, ...blockers, ...nonWorkingHours],
homeoffices
}
}
},
@@ -121,9 +172,9 @@ export default {
try {
const { subtasks, blockers } = await this.$axios.get_auth(`v1/subtasks?${queryParameters}`)
this.config.scrollHour = this.filter?.employee.work_start - 1
this.config.scrollHour = (this.filter?.employee.work_start / 60) - 1
this.config.date = this.filter.start
this.subtasks = [...subtasks, ...blockers.map(blocker => ({...blocker, type: 'blocker'}))]
this.subtasks = [...subtasks, ...blockers]
} catch (error) {
this.$toast.error('Nepodarilo sa nacitat data!')
}
@@ -83,7 +83,8 @@ export default {
_updateFilters() {
this.filter.start = this.date
this.filter.end = addDays(this.date, 1)
this.filter.end = this.date
// this.filter.end = addDays(this.date, 1)
this.selectedDate = String(this.filter.start?.toLocaleDateString('sk-SK'))
this._emitFilterChanged()
@@ -9,29 +9,44 @@ export default {
if (!this.subtasks || !this.blockers)
return
const blockers = this._generateBlockers()
console.log([...this._generateNonWorking(), ...this._generateTasks(), ...blockers.filter(blocker => blocker.type != 'home_office')])
return {
unscheduledTime: 333,
tasks: [...this._generateNonWorking(), ...this._generateTasks(), ...this._generateBlockers()]
tasks: [...this._generateNonWorking(), ...this._generateTasks(), ...blockers.filter(blocker => blocker.type != 'home_office')],
homeoffices: [...blockers.filter(blocker => blocker.type == 'home_office')]
}
},
splitDays() {
const days = []
const homeoffice = {}
this.filter.team.employees.forEach((employee, index) => {
let workingHours = 0
this.calendarData.tasks.forEach(task => {
if (task?.subtask?.employee_id == employee.id)
if ((task.subtask?.employee?.id != employee.id) && (task.blocker?.employee?.id != employee.id))
return
workingHours += timelineUtils._getDifferenceInHours(task.end, task.start)
})
this.calendarData.homeoffices.forEach(task => {
if (task.blocker.employee?.id != employee.id) return
homeoffice[task.split] = true
})
days.push({
id: index + 1,
name: employee.name,
fullname: `${employee.name} ${employee.surname}`,
initials: employee.name.charAt(0).toUpperCase() + employee.surname.charAt(0).toUpperCase(),
unscheduledTime: workingHours
unscheduledTime: workingHours,
isHomeoffice: homeoffice[index + 1] ? true : false
})
})
@@ -43,14 +58,14 @@ export default {
_generateNonWorking() {
const nonWorkingHours = []
const date = new Date(this.filter.start)
let earliestStartHour = null
let earliestStartMinutes = null
this.calendarConfig.date = date
const { start: dayStart, end: dayEnd } = this._getDayStartEnd()
this.filter.team.employees.forEach((employee, index) => {
if (!earliestStartHour || employee.work_start < earliestStartHour)
earliestStartHour = employee.work_start
if (!earliestStartMinutes || (employee.work_start) < earliestStartMinutes)
earliestStartMinutes = employee.work_start
nonWorkingHours.push({
@@ -72,7 +87,7 @@ export default {
})
})
this.calendarConfig.scrollHour = earliestStartHour - 1
this.calendarConfig.scrollHour = (earliestStartMinutes) - 1
return nonWorkingHours
},
@@ -107,7 +122,7 @@ export default {
initials: employee?.name?.charAt(0)?.toUpperCase() + employee?.surname?.charAt(0)?.toUpperCase(),
split: split,
project_code: subtask.project_code,
subtask: subtask,
subtask,
_timeblock_id: timeblock.id
})
}
@@ -136,7 +151,8 @@ export default {
split,
start: new Date(blocker.start),
end: new Date(blocker.end),
type: 'blocker',
type: blocker.type,
isBlocker: true,
initials: `${employee?.name[0]}${employee?.surname[0]}`,
color: '#B00020',
draggable: false,
@@ -1,7 +1,6 @@
<template>
<div>
<A-team-filter v-if="$store.getters['tm/isInitialized']" :unscheduledTime="calendarData?.unscheduledTime || 0" @filter-changed="onFilterChanged" />
<Z-timeline v-if="calendarData" :cal-data="calendarData" :cal-config="calendarConfig" :splits="splitDays" />
</div>
</template>
@@ -1,5 +1,5 @@
<template>
<v-row no-gutters class="align-center" v-if="employee">
<v-row no-gutters class="align-center">
<v-col cols="5">
<v-sheet class="py-3">
<div class="d-flex align-center">
@@ -12,9 +12,9 @@
<v-col cols="7">
<v-sheet class="py-3 d-flex align-center">
<p class="employee-selected-avatar d-flex justify-center align-center">
{{ employee._initials }}
{{ employee ? employee._initials : '?' }}
</p>
<p class="ml-2">{{ employee._fullName }}</p>
<p class="ml-2">{{ employee ? employee._fullName : $t('Unasigned') }}</p>
</v-sheet>
</v-col>
</v-row>
@@ -3,34 +3,28 @@ export default {
employeesOptions() {
const selectedEmployees = [...this.mastertask.employees].map(employee => employee.id)
const employees = this.enrichedEmployees.filter(
return this.enrichedEmployees.filter(
(employee) => !selectedEmployees.includes(employee.id)
) || []
},
return employees.map((employee) => {
return {
key: employee.id,
value: `${employee.name} ${employee.surname}`,
}
})
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)
}
},
methods: {
onProjectSelected({ key }) {
const project = this.projects.find(project => project.id == key)
this.mastertask.project_code = project.code
this.mastertask.project_id = project.id
},
// onProjectSelected(id) {
// const project = this.projects.find(project => project.id == id)
// this.mastertask.project_code = project.code
// this.mastertask.project_id = project.id
// },
onMasterTaskSelected({ key }) {
const mastertask = this.mastertasks.find(mastertask => mastertask.id == key)
this.subtask.master_task = mastertask
this.subtask.project_code = mastertask.project_code
},
onEmployeeSelected({ key }) {
const employee = this.enrichedEmployees.find(employee => employee.id == key)
onEmployeeSelected(id) {
const employee = this.enrichedEmployees.find(employee => employee.id == id)
this.mastertask.employees.push(employee)
this.showAddEmployeeInput = false
},
@@ -60,6 +54,6 @@ export default {
this.mastertask.team = this.teams.find(team => team.id == value)
this.mastertask.employees = this.mastertask.team.employees
}
},
}
}
@@ -8,17 +8,17 @@
</div>
<v-card-actions class="d-flex flex-column modal-content">
<Z-autocomplete label="Kód projektu" :options="projects.map((project) => ({ key: project.id, value: project.code }))" :validations="v$.mastertask.project_code" :clear-on-submit="false" :value="mastertask.project_code" :class="{ error: v$.mastertask.project_code.$errors.length }" class="modal-input" @option-selected="onProjectSelected" />
<v-autocomplete v-model="mastertask.project_id" :label="$t('Project code')" :class="{error: v$.mastertask.project_id.$errors.length}" :items="projectsOptions" item-title="code" item-value="id" variant="solo" class="modal-input" clearable @update:search="projectCodeSearch = $event" />
<v-text-field v-model="mastertask.name" @focus="onInputFocus" class="modal-input" label="Názov mastertasku" variant="solo" :class="{ error: v$.mastertask.name.$errors.length }" :hint="v$.mastertask.name.$errors ? errorUtils.parseErrors(v$.mastertask.name.$errors) : null"></v-text-field>
<Datepicker v-model="mastertask.deadline" @focus="onDateFocus" class="modal-input" format="dd.MM.yyyy, HH:mm" locale="sk" :class="{ error: v$.mastertask.deadline.$errors.length }" select-text="Vybrať" cancel-text="Zrušiť" placeholder="Deadline" />
<Datepicker v-model="mastertask.deadline" @focus="onDateFocus" :min-date="new Date()" class="modal-input" format="dd.MM.yyyy, HH:mm" locale="sk" :class="{ error: v$.mastertask.deadline.$errors.length }" select-text="Vybrať" cancel-text="Zrušiť" placeholder="Deadline" />
<v-text-field v-model="mastertask.duration" @focus="onInputFocus" class="modal-input mt-5" variant="solo" type="number" disabled :label="`${mastertask.duration} h`"></v-text-field>
<v-select v-model="this.mastertask.team_id" @focus="onInputFocus" class="modal-input border-select" :class="{ error: v$.mastertask.team_id.$errors.length }" label="Tím" variant="solo" :items="teams" item-value="id" item-title="name"></v-select>
<v-select v-model="mastertask.team_id" @focus="onInputFocus" class="modal-input border-select" :class="{ error: v$.mastertask.team_id.$errors.length }" label="Tím" variant="solo" :items="teams" item-value="id" item-title="name"></v-select>
<template v-if="!mastertask.id">
<template v-if="!mastertask.id && mastertask.team_id">
<v-text-field v-for="(employee, index) in mastertask.employees" :key="employee.id" @focus="onInputFocus" :class="{ error: !mastertask.employees[index].duration }" class="modal-input" :label="`${employee.name} ${employee.surname}`" variant="solo" placeholder="h" v-model="mastertask.employees[index].duration" type="number" min="1" @input="onEmployeeDurationChanged">
<p class="employee-select-avatar d-flex justify-center align-center">
{{ employee.name.charAt(0) + employee.surname.charAt(0) }}
@@ -26,7 +26,9 @@
</v-text-field>
</template>
<Z-autocomplete label="Vyhľadať zamestnanca" v-if="showAddEmployeeInput" :options="employeesOptions" class="modal-input" @option-selected="onEmployeeSelected" />
<v-autocomplete v-if="showAddEmployeeInput" :label="$t('Search for employee')" :class="{error: v$.mastertask.project_id.$errors.length}" :items="employeesOptions" item-title="_fullName" item-value="id" variant="solo" class="modal-input" clearable @update:model-value="onEmployeeSelected" />
<!-- <Z-autocomplete label="Vyhľadať zamestnanca" :options="employeesOptions" class="modal-input" @option-selected="onEmployeeSelected" /> -->
<v-btn class="w-100 mb-5 button secondary" :disabled="!mastertask.team?.id" @click="showAddEmployeeInput = true"> <v-icon class="mx-1" icon="mdi-plus" />Pridať zamestnanca </v-btn>
@@ -79,11 +81,11 @@ import ZFilesUpload from '@/plugins/tm@_components/z-files-upload.vue'
import useVuelidate from '@vuelidate/core'
import { required } from '@vuelidate/validators'
import errorUtils from '@/plugins/lib@auth/_utils/error.utils'
import ZAutocomplete from '@/plugins/tm@_components/z-autocomplete.vue'
// import ZAutocomplete from '@/plugins/tm@_components/z-autocomplete.vue'
import VuetifyFixFocusMixin from '@/plugins/tm@modal/_mixins/vuetify-fix-focus'
import moment from 'moment'
import { QuillEditor } from '@vueup/vue-quill'
import tmUtils from '@/plugins/tm@_utils/tm.utils'
// import tmUtils from '@/plugins/tm@_utils/tm.utils'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
@@ -94,7 +96,7 @@ import { mapGetters } from 'vuex'
export default {
// MIXINS
// computed: employeesOptions
// computed: employeesOptions, projectsOptions
// methods: onProjectSelected, onEmployeeSelected, onEmployeeDurationChanged
// watch: mastertask.team_id
@@ -106,7 +108,7 @@ export default {
ATasksTable,
Datepicker,
ZFilesUpload,
ZAutocomplete,
// ZAutocomplete,
QuillEditor
},
@@ -119,6 +121,7 @@ export default {
mastertask: null,
showAddEmployeeInput: false,
date: null,
projectCodeSearch: null
}
},
@@ -127,13 +130,29 @@ export default {
'teams',
'projects',
'enrichedEmployees'
])
]),
enrichedSubtasks() {
if (!this.mastertask?.subtasks)
return null
return this.mastertask.subtasks.map(subtask => {
const employee = this.enrichedEmployees.find(employee => employee.id == subtask.employee_id)
return {
...subtask,
_initials: employee ? employee.name.charAt(0) + employee.surname.charAt(0) : '?',
_fullName: employee ? `${employee.name} ${employee.surname}` : ''
}
})
}
},
validations() {
return {
mastertask: {
project_code: { required },
project_id: { required },
team_id: { required },
name: { required },
description: { required },
@@ -171,6 +190,7 @@ export default {
methods: {
async showModal({mastertask, editMode = false}) {
this.editMode = null
this.visible = true
this.mastertask = null
this.$nextTick(() => this.v$.$reset())
@@ -179,11 +199,12 @@ export default {
if (mastertask?.id) {
this.mastertask = await this.$axios.get_auth_data(`v1/mastertasks/${mastertask.id}`)
const project = this.projects.find((project) => project.code == this.mastertask?.project_code)
this.mastertask.team_id = this.mastertask.team?.id || null
// TODO zmazat ked zacne prichadzat project_id a team_id
const project = this.projects.find((project) => project.code == this.mastertask.project_code)
this.mastertask.project_id = project.id
this.mastertask.project_code = project.code
this.mastertask.team_id = this.mastertask.team?.id || null
// this.mastertask.project_code = project.code
} else {
this.mastertask = { employees: [], duration: 0 }
}
@@ -205,15 +226,6 @@ export default {
if (this.$refs['description'])
this.mastertask.description = this.$refs['description'].getHTML()
// this.mastertask.subtasks = this.mastertask.subtasks.map(subtask => {
// const employee = this.mastertask.employees.find(employee => employee.id == subtask.employee_id)
// subtask.duration = employee.duration
// return subtask
// })
// this.mastertask.project_code = null
try {
this.mastertask.deadline = moment(this.mastertask.deadline).format('YYYY-MM-DD HH:MM:SS')
@@ -1,9 +1,9 @@
<template>
<A-mastertask-name :name="subtask.master_task?.name || task.master_task" :id="subtask.master_task.id" :color="subtask.master_task.color" @close-modal="$emit('close-modal')" />
<A-mastertask-name :name="subtask._master_task.name" :id="subtask.master_task_id" :color="subtask._master_task.color" @close-modal="$emit('close-modal')" />
<A-mastertask-status :status="subtask.is_finished" />
<A-task-timeblocks :timeblocks="subtask.timeblocks" :subtask="subtask" />
<Z-created-by :employee-id="subtask.created_by" />
<A-task-assigned-employee :employee="subtask.employee" />
<A-task-assigned-employee :employee="subtask._employee" />
<A-task-planned-time :hours="subtask.duration" />
<A-task-remaining-time :hours="subtask.duration - subtask.spent_time" />
<A-task-spent-time :hours="subtask.spent_time" :subtask="subtask" />
+22 -55
View File
@@ -10,9 +10,10 @@
<v-card-actions class="d-flex flex-column modal-content">
<v-text-field v-model="subtask.project_code" label="Kód projektu" :value="subtask.project_code" @focus="onInputFocus" class="modal-input" variant="solo" disabled></v-text-field>
<Z-autocomplete label="Master task" :options="mastertasks.map((mastertask) => ({ key: mastertask.id, value: mastertask.name }))" :validations="v$.subtask.master_task.id" :clear-on-submit="false" :value="subtask?.master_task?.name" :class="{ error: v$.subtask.master_task.id.$errors.length }" class="modal-input" @option-selected="onMasterTaskSelected" />
<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 />
<Z-autocomplete label="Priradený" :value="subtask?.employee ? `${subtask?.employee?.name} ${subtask?.employee?.surname}` : ''" :options="enrichedEmployees.map(employee => ({key: employee.id, value: employee._fullName}))" :clear-on-submit="false" :class="{ error: v$.subtask.employee.$errors.length }" class="modal-input" @option-selected="onEmployeeSelected" />
<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>
<div class="w-100 modal-input">
@@ -28,8 +29,8 @@
<v-card v-else height="100%" class="d-flex flex-column scrolling">
<div class="d-flex justify-space-between align-center">
<div>
<p>{{ subtask.master_task?.project_code }}</p>
<h1>{{ subtask.master_task?.name }}</h1>
<p>{{ subtask._master_task.project_code }}</p>
<h1>{{ subtask._master_task.name }}</h1>
</div>
<v-icon @click.stop="closeModal()" icon="mdi-close" size="large" class="pb-5"></v-icon>
@@ -83,7 +84,7 @@ export default {
ZTaskReadonlyInfo,
ZFilesUpload,
QuillEditor,
ZAutocomplete
// ZAutocomplete
},
data() {
@@ -95,7 +96,6 @@ export default {
errorUtils,
v$: useVuelidate(),
preventInputFocus: false,
mastertaskIdSelect: null,
showDatePicker: false,
selectedNewDate: []
}
@@ -105,24 +105,15 @@ export default {
...mapGetters('tm', [
'mastertasks',
'enrichedEmployees'
]),
filteredEmployees() {
if (!this.enrichedEmployees || !this.subtask?.master_task) return []
const selectedEmployees = this.subtask.master_task.employees?.map((employee) => employee.id) || []
return this.enrichedEmployees.filter((employee) => !selectedEmployees.includes(employee.id))
}
])
},
validations() {
return {
subtask: {
master_task: {
id: { required }
},
master_task_id: { required },
duration: { required },
employee: { required }
employee_id: { required }
}
}
},
@@ -153,7 +144,6 @@ export default {
this.subtask = this._enrichTask(data)
} else {
this.subtask = { master_task: mastertask, timeblocks: [{start: tmUtils.formatDateTimeForBackend(moment()), end: tmUtils.formatDateTimeForBackend(moment())}] }
this.mastertaskIdSelect = mastertask.id
}
this.editMode = editMode || !this.subtask.id
@@ -178,7 +168,7 @@ export default {
const { subtask } = await this.$axios.put_auth(`v1/subtasks/${this.subtask.id}`, this.subtask)
this.$store.commit('tm/update_subtask', subtask)
} else {
const { subtask } = await this.$axios.post_auth(`v1/mastertasks/${this.subtask.master_task.id}/subtasks`, this.subtask)
const { subtask } = await this.$axios.post_auth(`v1/mastertasks/${this.subtask.master_task_id}/subtasks`, this.subtask)
this.subtask.id = subtask.id
this.$store.commit('tm/update_subtask', subtask)
}
@@ -190,17 +180,6 @@ export default {
}
},
async addNewDate() {
if (this.selectedNewDate) {
this.subtask.timeblocks.push({
start: this.selectedNewDate[0],
end: this.selectedNewDate[1]
})
this.showDatePicker = false
this.selectedNewDate = []
}
},
async downloadFile(id) {
try {
const file = await this.$axios.get_auth_data(`v1/subtasks/${this.subtask.id}`, { responseType: "arraybuffer" })
@@ -210,10 +189,6 @@ export default {
}
},
removeDate(index) {
this.subtask.timeblocks.splice(index, 1)
},
openDeleteTaskModal(subtask) {
this.$events.emit("show-delete-task-modal", { subtask })
},
@@ -226,35 +201,27 @@ export default {
this.subtask.files = files
},
onEmployeeSelected({ key }) {
this.subtask.employee_id = key
},
_enrichTask(subtask) {
subtask.master_task = this.mastertasks.find((mastertask) => mastertask.id == subtask.master_task_id)
subtask.employee = this.enrichedEmployees.find((employee) => employee.id == subtask.employee_id)
subtask._created_by = this.enrichedEmployees.find((employee) => employee.id == subtask.created_by)
this.mastertaskIdSelect = subtask.master_task?.id
return subtask
return {
...subtask,
_created_by: this.enrichedEmployees.find((employee) => employee.id == subtask.created_by),
_employee: this.enrichedEmployees.find((employee) => employee.id == subtask.employee_id),
_master_task: this.mastertasks.find((mastertask) => mastertask.id == subtask.master_task_id)
}
}
},
watch: {
mastertaskIdSelect(value) {
if (!value || !this.subtask) return
'subtask.employee_id'(value, oldValue) {
if (!value || !oldValue) return
this.subtask.master_task = this.mastertasks.find((mastertask) => mastertask.id == value)
this.subtask.project_code = this.subtask.master_task.project_code
this.subtask.timeblocks = []
this.subtask._employee = this.enrichedEmployees.find((employee) => employee.id == this.subtask.employee_id)
},
"subtask.employee_id"(value, oldValue) {
'subtask.master_task_id'(value) {
if (!value) return
if (value && oldValue)
this.subtask.timeblocks = []
this.subtask.employee = this.enrichedEmployees.find((employee) => employee.id == value)
this.subtask._master_task = this.mastertasks.find((mastertask) => mastertask.id == this.subtask.master_task_id)
},
editMode() {
+38 -69
View File
@@ -1,59 +1,33 @@
<template>
<v-dialog v-model="visible" max-width="750" max-height="90%" height="700px">
<W-loadable :content="team && allEmployees" class="h-100">
<W-loadable :content="team" class="h-100">
<v-card height="100%" class="d-flex flex-column">
<div class="d-flex justify-space-between align-center modal-header">
<h1>{{ team.id ? 'Upraviť tím' : 'Vytvoriť tím' }}</h1>
<v-icon
@click.stop="closeModal()"
icon="mdi-close"
size="large"
></v-icon>
<v-icon @click.stop="closeModal()" icon="mdi-close" size="large"></v-icon>
</div>
<v-card-actions class="d-flex flex-column modal-content">
<v-text-field
class="modal-input"
label="Názov tímu"
variant="solo"
:class="{ error: v$.team.name.$errors.length }"
:hint="
<v-text-field class="modal-input" label="Názov tímu" variant="solo"
:class="{ error: v$.team.name.$errors.length }" :hint="
v$.team.name.$errors
? errorUtils.parseErrors(v$.team.name.$errors)
: null
"
v-model="team.name"
></v-text-field>
" v-model="team.name"></v-text-field>
<div
class="relative w-100"
v-for="(employee, index) in team.employees"
:key="employee.id"
>
<v-text-field
class="modal-input"
disabled
label="Zamestanec"
variant="solo"
v-model="team.employees[index]._fullName"
>
<div class="relative w-100" v-for="(employee, index) in team.employees" :key="employee.id">
<v-text-field class="modal-input" disabled label="Zamestanec" variant="solo"
v-model="team.employees[index]._fullName">
</v-text-field>
<v-icon
class="remove-input"
@click.stop="team.employees.splice(index, 1)"
icon="mdi-close"
size="small"
></v-icon>
<v-icon class="remove-input" @click.stop="team.employees.splice(index, 1)" icon="mdi-close"
size="small"></v-icon>
</div>
<Z-autocomplete
label="Vyhľadať zamestnanca"
:options="employeesOptions"
:validations="v$.team.employees"
class="modal-input"
@option-selected="employeeSelected"
/>
<v-autocomplete v-model="employeeSelect" label="Priradený"
:class="{ error: v$.team.employees.$errors.length }" :items="employeesOptions"
item-title="_fullName" item-value="id" variant="solo" class="modal-input" clearable
ref="autocomplete" />
</v-card-actions>
<v-btn class="button primary block" @click="save()">{{ team.id ? 'UPRAVIŤ' : 'VYTVORIŤ' }}</v-btn>
@@ -63,31 +37,41 @@
</template>
<script>
import ZAutocomplete from '@/plugins/tm@_components/z-autocomplete.vue'
import useVuelidate from '@vuelidate/core'
import { required } from '@vuelidate/validators'
import errorUtils from '@/plugins/lib@auth/_utils/error.utils'
import tmUtils from '@/plugins/tm@_utils/tm.utils'
import { mapGetters } from 'vuex'
const atLeastOne = (value) => value.length
export default {
emits: ['close', 'update'],
components: {
ZAutocomplete,
},
data() {
return {
visible: false,
allEmployees: null,
v$: useVuelidate(),
errorUtils,
employeeSelect: null,
team: null
}
},
computed: {
...mapGetters('tm', [
'enrichedEmployees'
]),
employeesOptions() {
const selectedEmployeesIds = this.team.employees.map(
(employee) => employee.id
)
return this.enrichedEmployees.filter(employee => !selectedEmployeesIds.includes(employee.id))
}
},
validations() {
return {
team: {
@@ -104,9 +88,6 @@ export default {
this.$nextTick(() => this.v$.$reset())
try {
const allEmployees = await this.$axios.get_auth_data('v1/employees')
this.allEmployees = tmUtils.enrichEmployees(allEmployees)
if (team?.id) {
const t = await this.$axios.get_auth_data(`v1/teams/${team.id}`)
this.team = this._enrichTeam(t)
@@ -141,33 +122,21 @@ export default {
}
},
employeeSelected(selectedOption) {
const employee = this.allEmployees.find((empl) => empl.id == selectedOption.key)
this.team.employees.push(employee)
},
_enrichTeam(team) {
team.employees = tmUtils.enrichEmployees(team.employees)
return team
}
},
computed: {
employeesOptions() {
const selectedEmployeesIds = this.team.employees.map(
(employee) => employee.id
)
watch: {
employeeSelect(employeeId) {
if (!employeeId)
return
const employees = this.allEmployees.filter(
(employee) => !selectedEmployeesIds.includes(employee.id)
)
return employees.map((employee) => {
return {
key: employee.id,
value: `${employee.name} ${employee.surname}`,
}
})
const employee = this.enrichedEmployees.find(employee => employee.id == employeeId)
this.team.employees.push(employee)
this.employeeSelect = null
this.$refs.autocomplete.search = ''
}
}
}