<template> <v-container class="kanban"> <app-bar title="Kanban Board"></app-bar> <v-navigation-drawer v-model="rightDrawerState" touchless app right width="400" class="elevation-0" overlay-opacity="0.2" overlay-color="white"> <v-row class="mt-3" v-if="$vuetify.breakpoint.mdAndDown"> <v-btn depressed tile @click="rightDrawerState = !rightDrawerState" color="#E2E2E2" class="ml-3" :ripple="false"> <v-icon color="accent">mdi-chevron-right</v-icon> </v-btn> </v-row> <v-container class="my-5"> <v-card flat> <v-card-title class="text-h5">Cycles <dialog-info> <template v-slot:title>Cycles</template> <template v-slot:text> In the "cycles" area you get an overview of the ongoing and concluded cycles with respective kanban boards. </template> </dialog-info> </v-card-title> <v-card-text v-if="getBoard && getBoard !== null && getBoard.cycle !== null"> <v-list> <v-list-group color="accent" v-if="getCycleTodo.length > 0" :value="getCycleTodo.length > 0"> <template v-slot:activator> <v-list-item-icon> <v-icon>mdi-checkbox-multiple-marked-circle-outline</v-icon> </v-list-item-icon> <v-list-item-content> <v-list-item-title>Open Cycles</v-list-item-title> </v-list-item-content> </template> <v-list-item v-for="cycle in getCycleTodo" :key="cycle.id" link :to="{name: 'site-kanban-id', params: {'cycleId': cycle.id}}"> <v-list-item-content> <v-list-item-title>Cycle {{ cycle.id }}</v-list-item-title> </v-list-item-content> </v-list-item> </v-list-group> <v-list-group color="accent" :value="getCycleTodo.length === 0"> <template v-slot:activator> <v-list-item-icon> <v-icon>mdi-checkbox-multiple-marked-circle</v-icon> </v-list-item-icon> <v-list-item-content> <v-list-item-title>Finished Cycles</v-list-item-title> </v-list-item-content> </template> <v-list-item v-for="cycle in this.getCycleFinished" :key="cycle.id" link :to="{name: 'site-kanban-id', params: {'cycleId': cycle.id}}"> <v-list-item-content> <v-list-item-title>Cycle {{ cycle.id }}</v-list-item-title> </v-list-item-content> </v-list-item> </v-list-group> </v-list> </v-card-text> </v-card> </v-container> <v-container v-if="$store.getters['site/getSettings'].includes('deliverables')" class="my-5"> <v-card flat> <v-card-title class="text-h5"> Sub-Phase Recommendations <dialog-info> <template v-slot:title>Sub-Phase Recommendations</template> <template v-slot:text> The following list shows recommendations for (1) activities for the selected sub-phase and (2) methods for the respective activity. </template> </dialog-info> </v-card-title> <v-card-text v-if="getBoard && getBoard !== null && getBoard.cycle !== null"> <v-list> <v-list-group color="accent"> <template v-slot:activator> <v-list-item-icon> <v-icon>mdi-clipboard-list-outline</v-icon> </v-list-item-icon> <v-list-item-content> <v-list-item-title>Sub-Phase Activities</v-list-item-title> </v-list-item-content> </template> <v-list-item v-for="(del, i) in deliverables" :key="i"> <v-list-item-content> <v-list-item-title v-text="del.name"></v-list-item-title> </v-list-item-content> <v-list-item-action> <dialog-info> <template v-slot:title>{{ del.name }}</template> <template v-slot:text> {{ del.description }} </template> </dialog-info> </v-list-item-action> </v-list-item> </v-list-group> <v-list-group color="accent"> <template v-slot:activator> <v-list-item-icon> <v-icon>mdi-clipboard-check-outline</v-icon> </v-list-item-icon> <v-list-item-content> <v-list-item-title>Sub-Phase Methods</v-list-item-title> </v-list-item-content> </template> <v-list-item v-for="(del, i) in methods" :key="i"> <v-list-item-content> <v-list-item-title v-text="del.name"></v-list-item-title> </v-list-item-content> <v-list-item-action> <dialog-info> <template v-slot:title>{{ del.name }}</template> <template v-slot:text> {{ del.description }} </template> </dialog-info> </v-list-item-action> </v-list-item> </v-list-group> </v-list> </v-card-text> </v-card> </v-container> </v-navigation-drawer> <v-card flat class="mb-6"> <v-card-text class="black--text body-2"> Welcome to PETRA's Kanban Board! This is designed to simplify task management and enhance collaboration. With this Board, you can effortlessly create, assign, and track tasks, ensuring your team stays organized and productive. Visualize your workflow, set priorities, and streamline your prototyping process with ease. Empower your team to achieve your objectives efficiently. </v-card-text> </v-card> <v-row v-if="!getBoard" class="ma-1"> <v-col cols="3" :class="{'mr-auto':i < 4}" v-for="i in [1,2,3,4]" :key="i"> <v-card flat> <v-skeleton-loader class="white" type="card-heading, image"></v-skeleton-loader> </v-card> <v-card flat class="mt-5"> <v-skeleton-loader type="card-heading, image"></v-skeleton-loader> </v-card> </v-col> </v-row> <v-row v-if="getBoard && getBoard.id !== null"> <v-col cols="auto" class="text-h5 font-weight-bold mb-0" align-self="start"> Cycle {{ getBoard.id }} <cycle-details :cycle="getBoard.cycle"> <template v-slot:icon> <v-icon>mdi-information-outline</v-icon> </template> </cycle-details> </v-col> <v-col cols="auto" align-self="center"> <v-expand-transition> <v-btn v-if="!finished" depressed :color="this.valid ? 'secondary' : 'accent'" small @click="finishKanban" :loading="loading" class="px-8 body-2" height="24">Finish </v-btn> <v-btn v-else depressed :color="this.getBoard.cycle.status === 'Done' ? 'accent' : 'primary'" small @click="reopenCycle" :loading="loading" class="px-8 body-2" height="24">Reopen </v-btn> </v-expand-transition> </v-col> </v-row> <board v-if="getBoard && getBoard.id !== null" :tasks="getFilteredTasks" :team="getTeam" :finished="finished" ></board> <v-dialog v-model="dialogError" max-width="500px" overlay-color="white" style="z-index: 99" > <dialog-alert v-on:on-cancel="dialogError = false" v-on:on-continue="dialogError = false"> <template v-slot:title>Cycle Error</template> <template v-slot:text> No cycles found. </template> </dialog-alert> </v-dialog> <v-dialog v-model="cycleError" max-width="500px" overlay-color="white" style="z-index: 99" > <dialog-alert v-on:on-cancel="cycleError = false" v-on:on-continue="cycleError = false"> <template v-slot:title>Cycle Finish Error</template> <template v-slot:text> {{cycleErrorMsg}} </template> </dialog-alert> </v-dialog> </v-container> </template> <script> import {mapGetters} from "vuex"; import Board from "@/components/Board"; import AppBar from "@/components/AppBar"; import CycleDetails from "@/components/CycleDetails"; import DialogAlert from "@/components/DialogAlert"; import DialogInfo from "@/components/DialogInfo"; export default { name: "Kanban", components: { DialogInfo, DialogAlert, CycleDetails, AppBar, Board, }, props: ['cycleId'], data: () => ({ dialogError: false, cycleError: false, loading: false, cycleErrorMsg: "" }), methods: { fetchKanban() { this.destroyKanban() let url if (!this.cycleId) { url = '/board' } else { url = `/board?cycle-id=${this.cycleId}` } this.$http.get(url).then(response => { if (response.data.id === null) { // this happens if no cycles are available in database or access denied this.dialogError = true } this.$store.dispatch('site/setBoard', response.data) }) }, destroyKanban() { this.$store.dispatch('site/destroyBoard') }, finishKanban() { // Check if valid and display error if not if(!this.valid) { this.cycleError = true; return false; } this.loading = true this.$http.put(`/board?cycle-id=${this.getBoard.id}`, { id: this.getBoard.id, finished: true, }).then(response => { this.$store.dispatch('site/updateCycle', response.data.cycle) this.$router.push({name: 'site-dashboard'}) this.$snackbar.showMessage({ content: `Kanban Board of Cycle #${this.getBoard.id} is finished. Conclude the cycle by reflecting it.` }) }).finally(() => this.loading = false) }, reopenCycle() { // Check if it is reflected if(this.getBoard.cycle.status === 'Done') { this.cycleError = true; this.cycleErrorMsg = "You cannot reopened a reflected cycle"; return false; } this.loading = true this.$http.put(`/board?cycle-id=${this.getBoard.id}`, { id: this.getBoard.id, finished: false, }).then(response => { this.$store.dispatch('site/updateCycle', response.data.cycle) this.$snackbar.showMessage({ content: `Kanban Board of Cycle #${this.getBoard.id} is reopened. You can now edit it again.` }) this.getBoard.cycle.status = 'Ongoing'; }).finally(() => this.loading = false) }, }, computed: { ...mapGetters('site', [ 'getBoard', 'getTeam', 'getFilteredTasks', 'getLabelFilter', 'getRightDrawer', 'getCycleFinished', 'getCycleTodo', 'getDeliverables', 'getMethods']), rightDrawerState: { get() { return this.getRightDrawer }, set(val) { this.$store.dispatch('site/toggleRightDrawer', val) } }, finished() { return this.getBoard.cycle.status === 'Done' || this.getBoard.cycle.status === 'Reflect' }, valid() { // Check if board is finished // FINISHED IFF AT LEAST ONE TASK IN DONE FOR EACH LABEL // this.getBoard.labels is undefined at initial execution if (this.getBoard && typeof this.getBoard.tasks !== "undefined") { // Reduce list of labels.tasks into list of tasks if (this.getBoard.tasks.length === 0) { this.cycleErrorMsg = "You need more then zero tasks."; return false; } else if(this.getBoard.tasks.filter(task => task.status === "Todo").length > 0 || this.getBoard.tasks.filter(task => task.status === "Work-in-Progress").length > 0 || this.getBoard.tasks.filter(task => task.status === "Review").length > 0) { this.cycleErrorMsg = "You cannot have tasks left in Todo or Work-in-Progress."; return false; } else if(this.getBoard.tasks.filter(task => task.status === 'Done').length === 0) { this.cycleErrorMsg = "You need more than one task in Done to finish the cycle."; return false; } else if (!this.getBoard.tasks.filter(task => task.status === 'Done').every(task => task.effort_real > 0)) { this.cycleErrorMsg = "Each done task must have an effort assigned."; return false; } return true; } else { return false; } }, deliverables() { return this.getDeliverables.filter(deliverable => deliverable.phase.name === this.getBoard.cycle.phase) }, methods() { return this.getMethods.filter(method => method.phase.name === this.getBoard.cycle.phase) }, }, watch: { // eslint-disable-next-line no-unused-vars $route(to, from) { // console.log(to, from) if (!this.getBoard || to.params.cycleId !== this.getBoard.id) { // console.log('reload with', this.cycleId) this.fetchKanban() } }, }, created() { // fix timing issue with "v-if" i.e. make sure getBoard has data // https://stackoverflow.com/a/54974909 this.fetchKanban() }, } </script> <style scoped> </style>