petra-tool / frontend / src / components / AddCycle.vue
AddCycle.vue
Raw
<template>
  <v-dialog
    v-model="dialog"
    persistent
    max-width="1200"
    overlay-color="white"
    content-class="rounded-xl"
  >
    <template v-slot:activator="{ on, attrs }">
      <v-btn style="z-index: 10"
             color="primary"
             class="add-cycle"
             fab fixed bottom right
             v-bind="attrs"
             v-on="on">
        <v-icon large>mdi-plus</v-icon>
      </v-btn>
    </template>

    <dialog-alert v-on:on-cancel="onClose" v-on:on-continue="onClose" v-if="disabled">
      <template v-slot:text>
        Maximum number of pending cycles. Close old cycles first.
        <v-form v-show="false" ref="form" v-model="form.valid" lazy-validation></v-form>
      </template>
    </dialog-alert>

    <dialog-alert v-on:on-cancel="onClose" v-on:on-continue="onClose" v-else-if="addCycleDisabledFromSettings">
      <template v-slot:text>
        Creating new cycles is disabled. Finish your pending cycles first.
        <v-form v-show="false" ref="form" v-model="form.valid" lazy-validation></v-form>
      </template>
    </dialog-alert>

    <v-card v-else>
      <v-card-title class="accent white--text pt-10 pb-4 text-h5 pl-9">
        Add New Cycle
      </v-card-title>
      <v-card-text>
        <v-container fluid>
          <v-form ref="form" v-model="form.valid" lazy-validation>
            <v-row no-gutters>
              <v-col cols="12" class="text--primary body-2 my-5">
                Start your new prototyping cycle by planning your cycle with the following steps: (1) Select a project phase and confirm the prototype milestone, and (2) Define
                your objective (What do you want to answer?) and the definition of done for the cycle (What is your measure of when you have reached your objective?).
              </v-col>
            </v-row>

            <v-row>
              <v-col cols="12" md="6" class="py-0" :order="$store.getters['site/getSettings'].includes('phase') ? '' : '11'">
                <div class="text-h5 text--primary mb-3">Project Phase</div>
                <v-select
                  label="Choose a suitable project phase..."
                  :items="phaseOptions"
                  v-model="form.phaseSelected"
                  item-value="text"
                  return-object
                  outlined
                  :rules="[value => !!value || 'Phase is required.']"
                ></v-select>
              </v-col>
              <v-col cols="12" md="6" class="py-0" :order="$store.getters['site/getSettings'].includes('phase') ? '' : '12'">
                <div class="text-h5 text--primary mb-3">Prototype Milestone</div>
                <v-select
                  label="Confirm the prototype milestone..."
                  :items="milestoneOptions"
                  v-model="form.milestoneSelected"
                  outlined
                  :rules="[value => !!value || 'Milestone is required.']"
                ></v-select>
              </v-col>
              <v-col cols="12" class="py-0">
                <div class="text-h5 text--primary mb-3">Objective (What is your daily goal?)</div>
                <v-textarea
                  label="What is your desired outcome for today?"
                  v-model="form.objective"
                  outlined
                  auto-grow
                  rows="3"
                  :rules="[value => !!value || 'Objective is required.']"
                ></v-textarea>
              </v-col>
              <v-col cols="12" class="py-0">
                <div class="text-h5 text--primary mb-3">Definition of Done (What do you expect to achieve?)</div>
                <v-textarea
                  label="What do you need to accomplish to finish the cycle successfully?"
                  v-model="form.definition"
                  outlined
                  auto-grow
                  rows="3"
                  :rules="[value => !!value || 'Definition of Done is required.']"
                ></v-textarea>
              </v-col>
              <v-col cols="12" md="6" class="pt-0" v-if="$store.getters['site/getSettings'].includes('deliverables')">
                <v-expand-transition>
                  <div v-show="form.phaseSelected">
                    <div class="text-h5 text--primary mb-3">Activities
                      <a v-if="visibleDeliverables" class="body-2" @click="visibleDeliverables = false">hide...</a>
                      <a v-else class="body-2" @click="visibleDeliverables = true">show...</a>
                    </div>
                    <v-expand-transition>
                      <div v-show="visibleDeliverables">
                        <v-row no-gutters v-for="elem in deliverables" :key="elem.id">
                          <v-col cols="12">
                            <div>
                              <b>{{ elem.name }}</b>:
                              <span v-if="readMoreDeliverables[elem.id]">{{ elem.description }} </span>
                              <span v-else>{{ elem.description.substr(0, 120) }} </span>
                              <a v-if="!readMoreDeliverables[elem.id]" @click="showMore(elem.id, 'del')">more...</a>
                              <a v-else @click="showLess(elem.id, 'del')">less...</a>
                            </div>
                          </v-col>
                        </v-row>
                      </div>
                    </v-expand-transition>
                  </div>
                </v-expand-transition>
              </v-col>
              <v-col cols="12" md="6" class="pt-0" v-if="$store.getters['site/getSettings'].includes('deliverables')">
                <v-expand-transition>
                  <div v-show="form.phaseSelected">
                    <div class="text-h5 text--primary mb-3">Methods
                      <a v-if="visibleMethods" class="body-2" @click="visibleMethods = false">hide...</a>
                      <a v-else class="body-2" @click="visibleMethods = true">show...</a>
                    </div>
                    <v-expand-transition>
                      <div v-show="visibleMethods">
                        <v-row no-gutters v-for="elem in methods" :key="elem.id">
                          <v-col cols="12">
                            <div>
                              <b>{{ elem.name }}</b>:
                              <span v-if="readMoreMethods[elem.id]">{{ elem.description }} </span>
                              <span v-else>{{ elem.description.substr(0, 120) }} </span>
                              <template v-if="elem.description.length > 120">
                                <a v-if="!readMoreMethods[elem.id]" @click="showMore(elem.id, 'met')">more...</a>
                                <a v-else @click="showLess(elem.id, 'met')">less...</a>
                              </template>
                            </div>
                          </v-col>
                        </v-row>
                      </div>
                    </v-expand-transition>
                  </div>
                </v-expand-transition>
              </v-col>
            </v-row>
          </v-form>
        </v-container>
      </v-card-text>
      <v-card-actions class="pa-10">
        <v-btn
          text
          @click="onClose"
        >
          Cancel
        </v-btn>
        <v-spacer></v-spacer>
        <v-btn
          color="primary"
          depressed
          min-width="150"
          :loading="loading"
          @click="onSubmit"
        >
          Continue
        </v-btn>
      </v-card-actions>
    </v-card>

    <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>Validation Error</template>
        <template v-slot:text>
          Some fields are not valid. Fix errors to continue!
        </template>
      </dialog-alert>
    </v-dialog>

    <v-dialog
      v-model="dialogPhases"
      max-width="500px"
      overlay-color="white"
      style="z-index: 99"
    >
      <dialog-alert v-on:on-cancel="dialogPhases = false" v-on:on-continue="dialogPhases = false; ignorePhases = true; onSubmit()">
        <template v-slot:title>Linear Prototyping Error</template>
        <template v-slot:text>
          Do you really want to go back in phases as your progress will be reset?
        </template>
      </dialog-alert>
    </v-dialog>

  </v-dialog>
</template>

<script>
import DialogAlert from "@/components/DialogAlert";
import {mapGetters} from "vuex";

export default {
  name: "AddCycle",
  props: ['disabled'],
  components: {DialogAlert},
  data() {
    return {
      dialog: false,
      dialogError: null,
      dialogPhases: false,
      ignorePhases: false,
      form: {
        valid: true,
        objective: '',
        definition: '',
        phaseSelected: '',
        milestoneSelected: '',
      },
      loading: false,
      addCycleDisabledFromSettings: false,
      visibleDeliverables: false,
      visibleMethods: false,
      readMoreDeliverables: {},
      readMoreMethods: {},
      requirementsListDone: false,
    }
  },
  methods: {
    setErrors(res) {
      this.form.errors = res.data
    },
    onSubmit() {
      if (this.$refs.form.validate()) {
        this.errors = null
        this.loading = true

        this.$http.post(`/cycles?ignore=${ this.$store.getters['site/getSettings'].includes('linear') ? this.ignorePhases : 'false' }`, {
          objective: this.form.objective,
          definition: this.form.definition,
          phase: this.form.phaseSelected.text,
          milestone: this.form.milestoneSelected,
        }).then(response => {
          this.$store.dispatch('site/addCycle', {
            id: response.data.id,
            team: response.data.team,
            date_start: response.data.date_start,
            date_start_pretty: response.data.date_start_pretty,
            date_end: response.data.date_end,
            objective: this.form.objective,
            definition: this.form.definition,
            milestone: this.form.milestoneSelected,
            phase: this.form.phaseSelected.text,
            // finished: response.data.finished,
            // reflected: response.data.reflected,
            // completed: response.data.completed,
            status: response.data.status,
            expired: response.data.expired,
          })
          this.$snackbar.showMessage({
            content: `Cycle #${ response.data.id } added successfully. Go to Kanban Board to start working.`
          })
          this.dialog = false
          this.ignorePhases = false
          this.$store.dispatch('site/fetchProgress')
          this.$refs.form.reset()
        }).catch(error => {
          if (error.response.data) {
            // no going back in phases rule active, display error
            this.dialogPhases = true
          } else {
            this.$snackbar.showMessage({
              content: 'Something went wrong, please try again!', color: 'warning'
            })
          }
        }).finally(() => (this.loading = false))
      } else {
        this.dialogError = true
        this.loading = false
      }
    },
    onClose() {
      this.dialog = false
      this.$refs.form.reset()
    },
    showMore(id, type) {
      // https://stackoverflow.com/a/61381609
      if (type === 'del')
        this.$set(this.readMoreDeliverables, id, true)
      if (type === 'met')
        this.$set(this.readMoreMethods, id, true)
    },
    showLess(id, type) {
      if (type === 'del')
        this.$set(this.readMoreDeliverables, id, false)
      if (type === 'met')
        this.$set(this.readMoreMethods, id, false)
    },
  },
  computed: {
    ...mapGetters('site', ['getCycles', 'getTeam']),
    phaseOptions() {
      let disablePhaseRules = !this.$store.getters['site/getSettings'].includes('rules')
      let phaseOptions = [
        {
          header: 'Discover Phase',
        },
        {
          text: 'Problem Understanding',
        },
        {
          text: 'Problem Decomposition',
        },
        {divider: true},
        {
          header: 'Define Phase',
        },
        {
          text: 'Requirements Elicitation',
        },
        {
          text: 'Requirements Selection',
        },
      ]

      // if (disablePhaseRules || this.getCycles.some(cycle => cycle.phase === 'Requirements Selection' && cycle.status === 'Done'))
      if (disablePhaseRules || this.requirementsListDone)
        phaseOptions.push(
          {divider: true},
          {
            header: 'Develop Phase',
          },
          {
            text: 'Function Exploration',
          },
          {
            text: 'Embodiment Design',
          },
          {divider: true},
          {
            header: 'Launch Phase'
          },
          {
            text: 'Integration Design',
          },
          {
            text: 'User Testing and Feedback',
          },
        )

      return phaseOptions
    },
    milestoneOptions() {
      if (!this.form.phaseSelected || typeof this.form.phaseSelected === 'undefined' || typeof this.form.phaseSelected.text === 'undefined') {
        return []
      } else if (this.form.phaseSelected.text === 'Problem Understanding') {
        return [{
          text: 'Problem Pretotype - depiction of the problem',
          value: 'Problem Pretotype',
        }]
      } else if (this.form.phaseSelected.text === 'Problem Decomposition') {
        return [{
          text: 'Problem Prototype - broken down problem into sub-problems',
          value: 'Problem Prototype',
        }]
      } else if (this.form.phaseSelected.text === 'Requirements Elicitation') {
        return [{
          text: 'Solution Pretotype I - mocked-up representation',
          value: 'Solution Pretotype I',
        }]
      } else if (this.form.phaseSelected.text === 'Requirements Selection') {
        return [{
          text: 'Solution Pretotype II - mocked-up representation',
          value: 'Solution Pretotype II',
        }]
      } else if (this.form.phaseSelected.text === 'Function Exploration') {
        return [{
          text: 'Solution Prototype I - solution with features deployed',
          value: 'Solution Prototype I',
        }]
      } else if (this.form.phaseSelected.text === 'Embodiment Design') {
        return [{
          text: 'Solution Prototype II - solution without features visualized',
          value: 'Solution Prototype II',
        }]
      } else if (this.form.phaseSelected.text === 'Integration Design') {
        return [{
          text: 'Integration Prototype - integration of looks and functionality',
          value: 'Integration Prototype',
        }]
      } else if (this.form.phaseSelected.text === 'User Testing and Feedback') {
        return [{
          text: 'Final Prototype - sufficient features to satisfy early adopters',
          value: 'Final Prototype',
        }]
      } else {
        return [{text:'', value:''}]
      }
    },
    deliverables() {
      if (!this.form.phaseSelected || typeof this.form.phaseSelected === 'undefined' || typeof this.form.phaseSelected.text === 'undefined')
        return []
      else
        return this.$store.getters['site/getDeliverables'].filter(deliverable => deliverable.phase.name === this.form.phaseSelected.text)
    },
    methods() {
      if (!this.form.phaseSelected || typeof this.form.phaseSelected === 'undefined' || typeof this.form.phaseSelected.text === 'undefined')
        return []
      else
        return this.$store.getters['site/getMethods'].filter(method => method.phase.name === this.form.phaseSelected.text)
    }
  },
  watch: {
    dialog(visible) {
      if (visible) {
        // dialog was opened
        this.$http.get('/settings').then(res => {
          this.addCycleDisabledFromSettings = !!res.data.settings["addcycle"]
        })

        this.$http.get('/board/backlog').then(res => {
          // if requirements list is done, unlock the second half of phases
          this.requirementsListDone = res.data.requirements.some(req => req.finish)
        })
      } else {
        // dialog was closed
      }
    }
  }
}
</script>

<style lang="scss">
@import '~vuetify/src/styles/settings/_variables';

.v-subheader {
  font-size: 1.0rem;
  color: #68A694 !important;
}

.add-cycle {
  @media #{map-get($display-breakpoints, 'lg-and-up')} {
    margin-right: 400px
  }
}
</style>