mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 06:20:21 +00:00
feat(router, sidebar, modal): add 'Tasks Concepts' route and update sidebar/menu items; enhance task modal save functionality
This commit is contained in:
1 parent
205b3a7e42
commit
18bcf6706d
5 files changed
+387
-20
No files matched your search
@@ -52,18 +52,24 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/app/dashboard',
|
path: '/app/dashboard',
|
||||||
redirect: '/app/dashboard/tasks',
|
redirect: '/app/dashboard/tasks',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/app/dashboard/tasks',
|
path: '/app/dashboard/tasks',
|
||||||
name: 'Dashboard - Tasks',
|
name: 'Dashboard - Tasks',
|
||||||
meta: { title: 'Sub Tasks', requiresAuth: true },
|
meta: { title: 'Sub Tasks', requiresAuth: true },
|
||||||
component: () => import('@/plugins/tm@dashboard/tasks/tasks.vue')
|
component: () => import('@/plugins/tm@dashboard/tasks/tasks.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/app/dashboard/teams',
|
path: '/app/dashboard/tasks-concepts',
|
||||||
name: 'Dashboard - Teams',
|
name: 'Dashboard - Tasks Concepts',
|
||||||
meta: { title: 'Teams Overview', requiresAuth: true },
|
meta: { title: 'Draft Tasks', requiresAuth: true },
|
||||||
component: () => import('@/plugins/tm@dashboard/teams/teams.vue')
|
component: () => import('@/plugins/tm@dashboard/tasks-concepts/tasks-concepts.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/app/dashboard/teams',
|
||||||
|
name: 'Dashboard - Teams',
|
||||||
|
meta: { title: 'Teams Overview', requiresAuth: true },
|
||||||
|
component: () => import('@/plugins/tm@dashboard/teams/teams.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/app/dashboard/projects',
|
path: '/app/dashboard/projects',
|
||||||
name: 'Dashboard - Projects',
|
name: 'Dashboard - Projects',
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
:key="page.pageName"
|
:key="page.pageName"
|
||||||
:class="{ active: isActive(page.pageName) }"
|
:class="{ active: isActive(page.pageName) }"
|
||||||
@click="$router.push({ name: page.pageName }); closeMenu()"> {{ page.title }}
|
@click="$router.push({ name: page.pageName }); closeMenu()"> {{ page.title }}
|
||||||
<span
|
<span
|
||||||
v-if="page.pageName == 'Overview - Unscheduled tasks' && unscheduledSubtasks?.length >= 1"
|
v-if="page.pageName == 'Overview - Unscheduled tasks' && unscheduledSubtasks?.length >= 1"
|
||||||
class="mb-4 text-center rounded-circle badge">{{ unscheduledSubtasks?.length }}</span>
|
class="mb-4 text-center rounded-circle badge">{{ unscheduledSubtasks?.length }}</span>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
@@ -33,6 +33,10 @@ export default {
|
|||||||
title: 'Sub Tasks',
|
title: 'Sub Tasks',
|
||||||
pageName: 'Dashboard - Tasks'
|
pageName: 'Dashboard - Tasks'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Sub Tasks Concepts',
|
||||||
|
pageName: 'Dashboard - Tasks Concepts'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Teams overview',
|
title: 'Teams overview',
|
||||||
pageName: 'Dashboard - Teams'
|
pageName: 'Dashboard - Teams'
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<template>
|
||||||
|
<v-table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">
|
||||||
|
Kód projektu
|
||||||
|
</th>
|
||||||
|
<th class="text-left">Názov</th>
|
||||||
|
<th class="text-left">Deadline</th>
|
||||||
|
<th class="text-left">Najbližší neuzavretý</th>
|
||||||
|
<th class="text-left">Naplánované</th>
|
||||||
|
<th class="text-left">Zostávajúce</th>
|
||||||
|
<th class="text-left">Akcia</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
class="cursor-pointer"
|
||||||
|
v-for="subtask in subtasks"
|
||||||
|
:key="subtask.id"
|
||||||
|
@click.stop="editTask(subtask, false)"
|
||||||
|
>
|
||||||
|
<td>{{ subtask.project_code }}</td>
|
||||||
|
<td>{{ subtask.title ?? subtask.master_task }}</td>
|
||||||
|
<td v-if="subtask.deadline">{{ getDeadline(subtask) }}</td>
|
||||||
|
<v-tooltip text="Deadline z Mastertasku" location="end">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<td v-if="!subtask.deadline" v-bind="props">
|
||||||
|
{{ getDeadline(subtask) }}*
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<td>{{ displayTimeblock(subtask) }}</td>
|
||||||
|
<td>{{ subtask.duration }} h</td>
|
||||||
|
<td>{{ (subtask.duration - subtask.spent_time).toFixed(2) }} h</td>
|
||||||
|
|
||||||
|
<td class="task-action">
|
||||||
|
<div class="d-flex justify-start align-center">
|
||||||
|
<template v-if="subtask.type == 'office365'">
|
||||||
|
<img
|
||||||
|
class="pencil-icon"
|
||||||
|
:src="eye"
|
||||||
|
@click.stop="editTask(subtask, false)"
|
||||||
|
/>
|
||||||
|
<img class="office-icon" :src="office365" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<img
|
||||||
|
class="pencil-icon"
|
||||||
|
:src="pencil"
|
||||||
|
@click.stop="editTask(subtask, true)"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
class="pencil-icon"
|
||||||
|
:src="eye"
|
||||||
|
@click.stop="editTask(subtask, false)"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
class="trash-icon"
|
||||||
|
:src="bin"
|
||||||
|
@click.stop="deleteTask(subtask)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { bin, eye, office365, pencil } from '@/plugins/app/_img';
|
||||||
|
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
subtasks: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
eye,
|
||||||
|
bin,
|
||||||
|
office365,
|
||||||
|
pencil,
|
||||||
|
loadingSubtask: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters('tm', ['mastertasks']),
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
editTask(subtask, editMode = false) {
|
||||||
|
this.$events.emit('show-subtask-modal', { subtask, editMode });
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteTask(subtask) {
|
||||||
|
if (subtask.type == 'office365')
|
||||||
|
return this.$events.emit('show-unable-to-edit-office-task-modal', {
|
||||||
|
mode: 'delete',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$events.emit('show-delete-task-modal', { subtask });
|
||||||
|
},
|
||||||
|
|
||||||
|
getDeadline(subtask) {
|
||||||
|
if (subtask?.deadline)
|
||||||
|
return moment(subtask.deadline).format('DD.MM.yyyy');
|
||||||
|
|
||||||
|
return moment(subtask.master_task_deadline).format('DD.MM.yyyy');
|
||||||
|
},
|
||||||
|
|
||||||
|
getSelectedTimeblockDeadline(timeblocks) {
|
||||||
|
const currentDate = moment();
|
||||||
|
|
||||||
|
const unfinishedTimeblocks = timeblocks.filter(
|
||||||
|
(tb) => tb.is_finished === 0,
|
||||||
|
);
|
||||||
|
if (unfinishedTimeblocks.length > 0)
|
||||||
|
return moment(unfinishedTimeblocks[0].end).format('DD.MM.yyyy, HH:mm');
|
||||||
|
|
||||||
|
const upcomingTimeblocks = timeblocks
|
||||||
|
.filter((tb) => moment(tb.end).isAfter(currentDate))
|
||||||
|
.sort((a, b) => moment(a.end).diff(moment(b.end)));
|
||||||
|
|
||||||
|
if (upcomingTimeblocks.length > 0 && upcomingTimeblocks.is_finished === 0)
|
||||||
|
return moment(upcomingTimeblocks[0].end).format('DD.MM.yyyy, HH:mm');
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
displayTimeblock(subtask) {
|
||||||
|
const timeblockDeadline = this.getSelectedTimeblockDeadline(
|
||||||
|
subtask.timeblocks,
|
||||||
|
);
|
||||||
|
|
||||||
|
return timeblockDeadline !== '' ? timeblockDeadline : 'Žiadny termín';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
v-table, th, td
|
||||||
|
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity))
|
||||||
|
|
||||||
|
.office-icon
|
||||||
|
height: 20px
|
||||||
|
margin-left: 12px
|
||||||
|
|
||||||
|
@media screen and (max-width: 496px)
|
||||||
|
.task-action
|
||||||
|
div
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.pencil-icon, .trash-icon
|
||||||
|
border-radius: 50%
|
||||||
|
margin: 6px
|
||||||
|
padding: 6px
|
||||||
|
|
||||||
|
.disabled
|
||||||
|
opacity: 0.5
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
|
tbody
|
||||||
|
tr:hover
|
||||||
|
background: rgba(0,0,0,.1)
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Z-tasks-search
|
||||||
|
@search="onSearch"
|
||||||
|
@employee-selected="onEmployeeSelected"
|
||||||
|
@update-sort-order="onUpdateSortOrder"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<W-loadable
|
||||||
|
:content="filteredConceptTasks"
|
||||||
|
:emptyMessage="
|
||||||
|
searchQuery.length
|
||||||
|
? 'Neboli nájdené žiadne úlohy'
|
||||||
|
: 'Nenašli sa žiadne záznamy'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<A-concepts-table :subtasks="filteredConceptTasks" />
|
||||||
|
</W-loadable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ZTasksSearch from '@/plugins/tm@overview/_components/z-tasks-search.vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import AConceptsTable from './_components/a-concepts-table.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ZTasksSearch,
|
||||||
|
AConceptsTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchQuery: '',
|
||||||
|
sortOrder: null,
|
||||||
|
conceptSubtasks: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async ionViewWillEnter() {
|
||||||
|
await this.loadConceptTasks();
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.loadConceptTasks();
|
||||||
|
|
||||||
|
this.$events.on('on-subtask-edited', async () => {
|
||||||
|
await this.loadConceptTasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$events.on('on-subtask-deleted', async () => {
|
||||||
|
await this.loadConceptTasks();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
this.$events.off('on-subtask-edited');
|
||||||
|
this.$events.off('on-subtask-deleted');
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters('tm', ['mastertasks']),
|
||||||
|
|
||||||
|
filteredConceptTasks() {
|
||||||
|
if (!this.conceptSubtasks) return null;
|
||||||
|
|
||||||
|
let filteredSubtasks = this.conceptSubtasks.filter((subtask) => {
|
||||||
|
return (
|
||||||
|
this.filterParams(
|
||||||
|
this.searchQuery,
|
||||||
|
subtask.master_task?.name || subtask.title,
|
||||||
|
) || this.filterParams(this.searchQuery, subtask.project_code)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Sorting by name A-Z
|
||||||
|
if (this.sortOrder === 'nameAsc') {
|
||||||
|
filteredSubtasks.sort((a, b) => {
|
||||||
|
const nameA = (a.title || a.master_task || '').toLowerCase();
|
||||||
|
const nameB = (b.title || b.master_task || '').toLowerCase();
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
});
|
||||||
|
//Sorting by name Z-A
|
||||||
|
} else if (this.sortOrder === 'nameDesc') {
|
||||||
|
filteredSubtasks.sort((a, b) => {
|
||||||
|
const nameA = (a.title || a.master_task || '').toLowerCase();
|
||||||
|
const nameB = (b.title || b.master_task || '').toLowerCase();
|
||||||
|
return nameB.localeCompare(nameA);
|
||||||
|
});
|
||||||
|
//Sorting by date Newest-Oldest
|
||||||
|
} else if (this.sortOrder === 'deadlineDesc') {
|
||||||
|
filteredSubtasks.sort((a, b) => {
|
||||||
|
const deadlineA = a.deadline
|
||||||
|
? new Date(a.deadline)
|
||||||
|
: new Date(this.getMasterTaskDeadline(a));
|
||||||
|
const deadlineB = b.deadline
|
||||||
|
? new Date(b.deadline)
|
||||||
|
: new Date(this.getMasterTaskDeadline(b));
|
||||||
|
return deadlineB - deadlineA;
|
||||||
|
});
|
||||||
|
//Sorting by date Oldest-Newest
|
||||||
|
} else if (this.sortOrder === 'deadlineAsc') {
|
||||||
|
filteredSubtasks.sort((a, b) => {
|
||||||
|
const deadlineA = a.deadline
|
||||||
|
? new Date(a.deadline)
|
||||||
|
: new Date(this.getMasterTaskDeadline(a));
|
||||||
|
const deadlineB = b.deadline
|
||||||
|
? new Date(b.deadline)
|
||||||
|
: new Date(this.getMasterTaskDeadline(b));
|
||||||
|
return deadlineA - deadlineB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredSubtasks;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadConceptTasks(employeeId = null) {
|
||||||
|
try {
|
||||||
|
const params = { type: 'concept' };
|
||||||
|
if (employeeId) {
|
||||||
|
params.employee_id = employeeId;
|
||||||
|
} else {
|
||||||
|
params.employee_id = this.$store.getters['auth/user'].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParameters = new URLSearchParams(params).toString();
|
||||||
|
const { subtasks } = await this.$axios.get_auth(
|
||||||
|
`v1/subtasks?${queryParameters}`,
|
||||||
|
);
|
||||||
|
this.conceptSubtasks = subtasks;
|
||||||
|
} catch (error) {
|
||||||
|
this.$toast.error('Nepodarilo sa načítať koncepty!');
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development')
|
||||||
|
console.log('[tasks-concepts.vue] Error: ', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearch(searchQuery) {
|
||||||
|
this.searchQuery = searchQuery;
|
||||||
|
},
|
||||||
|
|
||||||
|
async onEmployeeSelected(id) {
|
||||||
|
await this.loadConceptTasks(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateSortOrder(newSortOrder) {
|
||||||
|
this.sortOrder = newSortOrder;
|
||||||
|
},
|
||||||
|
|
||||||
|
filterParams(searchQuery, row) {
|
||||||
|
if (!row) return false;
|
||||||
|
return row.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
},
|
||||||
|
|
||||||
|
getMasterTaskDeadline(subtask) {
|
||||||
|
return subtask.master_task_deadline;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -273,12 +273,18 @@
|
|||||||
/>
|
/>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
<v-btn class="button primary" v-if="!isSaving" @click="save()">{{
|
<v-btn class="button primary" :disabled="isSaving" @click="save()">{{
|
||||||
subtask.id ? 'UPRAVIŤ' : 'VYTVORIŤ'
|
subtask.id ? 'UPRAVIŤ' : 'VYTVORIŤ'
|
||||||
}}</v-btn>
|
}}</v-btn>
|
||||||
<v-btn class="button primary disabled" v-else disabled>{{
|
|
||||||
$t('Ukladám...')
|
<template v-if="!subtask.id">
|
||||||
}}</v-btn>
|
<v-btn
|
||||||
|
class="button primary outline mt-2"
|
||||||
|
@click="save()"
|
||||||
|
:disabled="isSaving"
|
||||||
|
>ULOŽIŤ KONCEPT</v-btn
|
||||||
|
>
|
||||||
|
</template>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-card v-else height="100%" class="d-flex flex-column scrolling">
|
<v-card v-else height="100%" class="d-flex flex-column scrolling">
|
||||||
@@ -699,7 +705,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async save() {
|
async save(concept = false) {
|
||||||
this.wasFormValidationTriggered = true;
|
this.wasFormValidationTriggered = true;
|
||||||
if (!(await this.validateAllFields())) return;
|
if (!(await this.validateAllFields())) return;
|
||||||
|
|
||||||
@@ -731,9 +737,14 @@ export default {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const payload = { ...this.subtask, timesheets, timeblocks };
|
||||||
|
if (concept) {
|
||||||
|
payload.concept = true;
|
||||||
|
}
|
||||||
|
|
||||||
const { subtask } = await this.$axios.patch_auth_form(
|
const { subtask } = await this.$axios.patch_auth_form(
|
||||||
`v1/subtasks/${this.subtask.id}`,
|
`v1/subtasks/${this.subtask.id}`,
|
||||||
{ ...this.subtask, timesheets, timeblocks },
|
payload,
|
||||||
);
|
);
|
||||||
localStorage.removeItem('subtaskData');
|
localStorage.removeItem('subtaskData');
|
||||||
|
|
||||||
@@ -765,6 +776,9 @@ export default {
|
|||||||
...this.subtask,
|
...this.subtask,
|
||||||
employees: validEmployees,
|
employees: validEmployees,
|
||||||
};
|
};
|
||||||
|
if (concept) {
|
||||||
|
subtaskCopy.concept = true;
|
||||||
|
}
|
||||||
delete subtaskCopy._master_task;
|
delete subtaskCopy._master_task;
|
||||||
const { subtasks } = await this.$axios.post_auth(
|
const { subtasks } = await this.$axios.post_auth(
|
||||||
`v1/mastertasks/${this.subtask.master_task_id}/subtasks`,
|
`v1/mastertasks/${this.subtask.master_task_id}/subtasks`,
|
||||||
|
|||||||
Reference in new issue
Block a user