valasek/timesheet

View on GitHub
client/src/pages/Overview.vue

Summary

Maintainability
Test Coverage
<!-- Copyright © 2018-2020 Stanislav Valasek <valasek@gmail.com> -->

<template>
  <q-page padding>
    <q-toolbar class="q-pa-md bg-primary">
      <change-week />
      <div class="q-gutter-x-md">
        <select-consultant class="q-gutter-x-md" />
      </div>
    </q-toolbar>
    <div class="row items-baseline">
      <div class="column q-pa-md ">
        <q-toolbar class="bg-primary text-secondary">
          <q-toolbar-title>Week - {{ thisWeek }} </q-toolbar-title>
        </q-toolbar>
        <div class="row">
            <q-card-section>
              <div class="text-subtitle1">
                Reported Time
              </div>
              <q-table :columns="headersNumbers" :data="weeklyWorkingTimeOverview" :pagination.sync="myPagination"
                       hide-bottom
              >
                <template slot="items" slot-scope="props">
                  <td v-if="props.item.value !== ''" class="text-xs-left">
                    {{ props.item.text }}
                  </td>
                  <td v-if="props.item.value !== ''" class="text-xs-left">
                    {{ props.item.value }}
                  </td>
                  <td v-if="props.item.value !== ''" class="text-xs-left">
                    {{ props.item.value }}
                  </td>
                </template>
              </q-table>
            </q-card-section>
        </div>
        <div class="row">
          <q-card-section>
            <div class="text-subtitle1">
              Reported Projects
            </div>
            <q-table :columns="headersProjects" :data="weeklyProjectsOverview" :pagination.sync="myPagination"
                      hide-bottom
            >
              <template slot="items" slot-scope="props">
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.text }}
                </td>
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.value }}
                </td>
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.value }}
                </td>
              </template>
            </q-table>
          </q-card-section>
        </div>
      </div>
      <div class="column q-pa-md ">
        <q-toolbar class="bg-primary text-secondary">
          <q-toolbar-title>Month - {{ selectedMonth | formatMonth }}</q-toolbar-title>
        </q-toolbar>
        <div class="row">
          <q-card-section>
            <div class="text-subtitle1">
              Reported Time
            </div>
            <q-table :columns="headersNumbers" :data="monthlyWorkingTimeOverview" :pagination.sync="myPagination"
                      hide-bottom
            >
              <template slot="items" slot-scope="props">
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.text }}
                </td>
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.value }}
                </td>
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.value }}
                </td>
              </template>
            </q-table>
          </q-card-section>
        </div>
        <div class="row">
          <q-card-section>
            <div class="text-subtitle1">
              Reported Projects
            </div>
            <q-table :columns="headersProjects" :data="monthlyProjectsOverview" :pagination.sync="myPagination"
                      hide-bottom
            >
              <template slot="items" slot-scope="props">
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.text }}
                </td>
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.value }}
                </td>
                <td v-if="props.item.value !== ''" class="text-xs-left">
                  {{ props.item.value }}
                </td>
              </template>
            </q-table>
          </q-card-section>
        </div>
      </div>

      <div class="column q-pa-md">
        <!-- <q-card flat class="row q-pa-md items-baseline"> -->
        <q-toolbar class="my-toolbar bg-primary text-secondary">
          <q-toolbar-title>Year - {{ thisYear }}</q-toolbar-title>
        </q-toolbar>
        <q-card-section>
          <div class="text-subtitle1">
            Vacations
          </div>
          <q-table :columns="headersV" :data="vacations" hide-bottom>
            <template slot="items" slot-scope="props">
              <td class="text-xs-left">
                {{ props.item.text }}
              </td>
              <td class="text-xs-left">
                {{ props.item.value }}
              </td>
              <td class="text-xs-left">
                {{ props.item.value }}
              </td>
            </template>
          </q-table>
        </q-card-section>
        <q-card-section>
          <div class="text-subtitle1">
            Personal Days
          </div>
          <q-table :columns="headersP" :data="personalDays" hide-bottom>
            <template slot="items" slot-scope="props">
              <td class="text-xs-left">
                {{ props.item.text }}
              </td>
              <td class="text-xs-left">
                {{ props.item.value / dailyWorkingHours }}
              </td>
              <td class="text-xs-left">
                {{ props.item.value }}
              </td>
            </template>
          </q-table>
        </q-card-section>
        <q-card-section>
          <div class="text-subtitle1">
            Sick Days
          </div>
          <q-table :columns="headersS" :data="sickDays" hide-bottom>
            <template slot="items" slot-scope="props">
              <td class="text-xs-left">
                {{ props.item.text }}
              </td>
              <td class="text-xs-left">
                {{ props.item.value }}
              </td>
              <td class="text-xs-left">
                {{ props.item.value }}
              </td>
            </template>
          </q-table>
        </q-card-section>
      <!-- </q-card> -->
      </div>
    <!-- <div class="row">
      <q-toolbar class="q-pa-md bg-grey-3">
        <q-toolbar-title>Available minus reported hours in {{ selectedMonth | formatMonth }}: {{ balancePeriod }}</q-toolbar-title>
      </q-toolbar>
      <q-card flat>
        <q-card-section>
            Balance: <span :class="textColor(getBalance())">
              {{ getBalance() | pluralizeHour }}</span>
        </q-card-section>
      </q-card>
    </div> -->
    </div>
  </q-page>
</template>

<script>
import { mapState } from 'vuex'
import { format, lightFormat, getYear, differenceInBusinessDays, addDays, startOfMonth, endOfMonth } from 'date-fns'
import { workHoursMixin } from '../mixins/workHoursMixin'
// import selectConsultant from '../components/SelectConsultant'
// import changeWeek from '../components/ChangeWeek'

export default {

  components: {
    /* webpackChunkName: "core" */
    'select-consultant': () => import('components/SelectConsultant'),
    /* webpackChunkName: "core" */
    'change-week': () => import('components/ChangeWeek')
  },

  filters: {
    formatMonth: function (date) {
      if (!date) return ''
      return format(date, 'MMMM')
    },
    formatDay: function (date) {
      if (!date) return ''
      return format(date, 'MM/dd')
    },
    pluralizeHour: function (count) {
      if (count === 0) {
        return '0 hours'
      } else if (count === 1) {
        return '1 hour'
      } else {
        return count + ' hours'
      }
    }
  },

  mixins: [ workHoursMixin ],

  data () {
    return {
      balancePeriod: '',
      filter: '',
      myPagination: { 'rowsPerPage': 50 },
      headersNumbers: [
        { label: '', align: 'left', field: 'text', sortable: false },
        { label: 'Days', align: 'left', field: 'value', format: val => `${val}` / this.dailyWorkingHours, sortable: false },
        { label: 'Hours', align: 'left', field: 'value', sortable: false }
      ],
      headersProjects: [
        { label: '', align: 'left', field: 'text', sortable: false },
        { label: 'Days', align: 'left', field: 'value', format: val => `${val}` / this.dailyWorkingHours, sortable: false },
        { label: 'Hours', align: 'left', field: 'value', sortable: false }
      ],
      headersV: [
        { label: '', field: 'text', sortable: false },
        { label: 'Days', align: 'left', field: 'value', format: val => `${val}` / this.dailyWorkingHours, sortable: false },
        { label: 'Hours', align: 'left', field: 'value', sortable: false }
      ],
      headersP: [
        { label: '', field: 'text', sortable: false },
        { label: 'Days', align: 'left', field: 'value', format: val => `${val}` / this.dailyWorkingHours, sortable: false },
        { label: 'Hours', align: 'left', field: 'value', sortable: false }
      ],
      headersS: [
        { label: '', field: 'text', sortable: false },
        { label: 'Days', align: 'left', field: 'value', format: val => `${val}` / this.dailyWorkingHours, sortable: false },
        { label: 'Hours', align: 'left', field: 'value', sortable: false }
      ]
    }
  },

  computed: {
    ...mapState({
      reportedHours: state => state.reportedHours.consultantMonthly,
      reportedHoursSummary: state => state.reportedHours.summary,
      rates: state => state.rates.all,
      projects: state => state.projects.all,
      dateFrom: state => state.settings.dateFrom,
      dateTo: state => state.settings.dateTo,
      selectedMonth: state => state.settings.selectedMonth,
      selectedConsultant: state => state.consultants.selected,
      selectedAllocation: state => state.consultants.selectedAllocation,
      dailyWorkingHours: state => state.settings.dailyWorkingHours,
      yearlyVacationDays: state => state.settings.yearlyVacationDays,
      vacation: state => state.settings.vacation,
      yearlyPersonalDays: state => state.settings.yearlyPersonalDays,
      vacationPersonal: state => state.settings.vacationPersonal,
      yearlySickDays: state => state.settings.yearlySickDays,
      vacationSick: state => state.settings.vacationSick,
      isWorking: state => state.settings.isWorking,
      isNonWorking: state => state.settings.isNonWorking,
      holidays: state => state.holidays.all
    }),
    startOfMonth () {
      return startOfMonth(new Date())
    },
    yesterday () {
      return addDays(new Date(), -1)
    },
    thisYear () { return getYear(this.selectedMonth) },
    thisWeek () {
      return format(this.dateFrom, 'MMM d') + ' - ' + format(this.dateTo, 'MMM d')
    },
    vacations () {
      return [
        {
          text: 'Total',
          value: this.yearlyVacationDays * this.dailyWorkingHours
        },
        {
          text: 'Remaining',
          value: this.yearlyVacationDays * this.dailyWorkingHours - this.getTotalsForRate(this.reportedHoursSummary, this.vacation)
        },
        {
          text: 'Reported',
          value: this.getTotalsForRate(this.reportedHoursSummary, this.vacation)
        }
      ]
    },
    personalDays () {
      return [
        {
          text: 'Total',
          value: this.yearlyPersonalDays * this.dailyWorkingHours
        },
        {
          text: 'Remaining',
          value: this.yearlyPersonalDays * this.dailyWorkingHours - this.getTotalsForRate(this.reportedHoursSummary, this.vacationPersonal)
        },
        {
          text: 'Reported',
          value: this.getTotalsForRate(this.reportedHoursSummary, this.vacationPersonal)
        }
      ]
    },
    sickDays () {
      return [
        {
          text: 'Total',
          value: this.yearlySickDays * this.dailyWorkingHours
        },
        {
          text: 'Remaining',
          value: this.yearlySickDays * this.dailyWorkingHours - this.getTotalsForRate(this.reportedHoursSummary, this.vacationSick)
        },
        {
          text: 'Reported',
          value: this.getTotalsForRate(this.reportedHoursSummary, this.vacationSick)
        }
      ]
    },
    weeklyWorkingTimeOverview () {
      return this.workingTimeOverview('week')
    },
    weeklyProjectsOverview () {
      return this.projectsOverview('week')
    },
    monthlyWorkingTimeOverview () {
      return this.workingTimeOverview('month')
    },
    monthlyProjectsOverview () {
      return this.projectsOverview('month')
    }
  },

  watch: {
    selectedConsultant (newValue, oldValue) {
      this.$store.dispatch('reportedHours/getMonthlyData', { date: this.selectedMonth, consultant: this.selectedConsultant })
    }
  },

  created () {
    this.$store.commit('context/SET_PAGE', 'Overview of reported hours')
    this.$store.commit('context/SET_PAGE_ICON', 'show_chart')
    this.$store.dispatch('reportedHours/getYearlySummary', this.selectedMonth)
  },

  methods: {
    textColor (item) {
      let colorClass = ''
      if (item < 0) { colorClass = 'red--text lighten-2' }
      return colorClass
    },
    getBalance () {
      const today = new Date()
      if (this.selectedMonth.getMonth() !== today.getMonth() || format(today, 'd') === '1') {
        this.balancePeriod = this.$options.filters.formatDay(startOfMonth(this.selectedMonth)) + ' - ' + this.$options.filters.formatDay(endOfMonth(this.selectedMonth))
        return this.monthlyWorkingTimeOverview[4].value * -1
      }
      const worked = this.getTotalsInPeriod(this.selectedReportedHoursInPeriod(this.startOfMonth, this.yesterday))
      const available = this.workingDaysInPeriod(this.startOfMonth, this.yesterday)
      this.balancePeriod = this.$options.filters.formatDay(this.startOfMonth) + ' - ' + this.$options.filters.formatDay(this.yesterday)
      return (worked.working + worked.nonWorking - available * this.dailyWorkingHours) * this.selectedAllocation
    },
    workingDaysInPeriod (dFrom, dTo) {
      const diff = differenceInBusinessDays(dTo, dFrom) + 1
      const holidays = this.getHolidays(dFrom, dTo)
      return (diff - holidays)
    },
    selectedReportedHoursInPeriod (from, to) {
      return this.reportedHours.filter(function (report) {
        const d = new Date(report.date)
        return (d >= from && d <= to)
      })
    },
    projectsOverview (period) {
      var summary = {}
      switch (period) {
        case 'week':
          summary = this.getProjectTotalsInWeek(this.selectedReportedHoursInPeriod(this.dateFrom, this.dateTo))
          break
        case 'month':
          summary = this.getProjectTotalsInMonth(this.reportedHoursSummary)
          break
        default:
          // console.log('workingTimeOverview unknown period:', period) /* eslint-disable-line no-console */
      }
      return summary
    },
    workingTimeOverview (period) {
      var data = []
      var summary = {}
      switch (period) {
        case 'week':
          summary = this.getTotalsInPeriod(this.selectedReportedHoursInPeriod(this.dateFrom, this.dateTo))
          data.push({
            text: 'Available working time',
            value: this.workingDaysInPeriod(this.dateFrom, this.dateTo) * this.dailyWorkingHours
          })
          break
        case 'month':
          data.push({
            text: 'Available working time',
            value: this.workingDaysInPeriod(startOfMonth(this.selectedMonth), endOfMonth(this.selectedMonth)) * this.dailyWorkingHours
          })
          summary = this.getTotalInMonth(this.reportedHoursSummary)
          break
        default:
          // console.log('workingTimeOverview unknown period:', period) /* eslint-disable-line no-console */
      }
      data.push(
        {
          text: 'Reported total',
          value: summary.working + summary.nonWorking
        },
        {
          text: ' — Reported working time',
          value: summary.working
        },
        {
          text: ' — Reported non-working time',
          value: summary.nonWorking
        }
      )
      switch (period) {
        case 'week':
          data.push({
            text: 'Remaining total',
            value: this.workingDaysInPeriod(this.dateFrom, this.dateTo) * this.dailyWorkingHours - (summary.working + summary.nonWorking)
          })
          break
        case 'month':
          data.push({
            text: 'Remaining total',
            value: this.workingDaysInPeriod(startOfMonth(this.selectedMonth), endOfMonth(this.selectedMonth)) * this.dailyWorkingHours - (summary.working + summary.nonWorking)
          })
          break
        default:
          // console.log('workingTimeOverview unknown period:', period) /* eslint-disable-line no-console */
      }
      return data
    },
    getDaysInMonth (month, year) {
      let date = new Date(year, month, 0).getDate()
      return date
    },
    isWeekday (year, month, day) {
      let date = new Date(year, month, day).getDay()
      return date !== 0 && date !== 6
    },
    getWeekdaysInMonth (month, year) {
      month = '12'
      year = '2018'
      let days = this.getDaysInMonth(month, year)
      let weekdays = 0
      for (let i = 0; i < days; i++) {
        if (this.isWeekday(year, month, i + 1)) weekdays++
      }
      return weekdays
    },
    getTotalsForRate (hours, rate) {
      const consultant = this.selectedConsultant
      let h = hours.reduce(
        function (total, current) {
          let h = 0
          if (current.rate === rate && current.consultant === consultant) { h = current.hours }
          return total + h
        }, 0)
      return h || 0
    },
    getTotalInMonth (hours) {
      let working = 0
      let nonWorking = 0
      const consultant = this.selectedConsultant
      const month = lightFormat(this.selectedMonth, 'M')
      hours.forEach(function (element) {
        if (element.month === month && element.consultant === consultant) {
          var el = this.rates.find(o => o.name === element.rate)
          if (el !== undefined && el.type === this.isWorking) { working += element.hours }
          if (el !== undefined && el.type === this.isNonWorking) { nonWorking += element.hours }
        }
      }, this)
      return { working: working, nonWorking: nonWorking }
    },
    getTotalsInPeriod (hours) {
      let working = 0
      let nonWorking = 0
      hours.forEach(function (element) {
        var el = this.rates.find(o => o.name === element.rate)
        if (el !== undefined && el.type === this.isWorking) { working += element.hours }
        if (el !== undefined && el.type === this.isNonWorking) { nonWorking += element.hours }
      }, this)
      return { working: working, nonWorking: nonWorking }
    },
    getProjectTotalsInWeek (hours) {
      const projects = this.projects.map(function (project) {
        return { text: project.name,
          value: hours.reduce(
            function (total, current) {
              let h = 0
              current.project === project.name ? h = current.hours : h = 0
              return total + h
            }, 0) }
      }).filter(item => item.value > 0)
      return projects
    },
    getProjectTotalsInMonth (hours) {
      const consultant = this.selectedConsultant
      const month = lightFormat(this.selectedMonth, 'M')
      const projects = this.projects.map(function (project) {
        return { text: project.name,
          value: hours.filter(record => (record.consultant === consultant && record.month === month)).reduce(
            function (total, current) {
              let h = 0
              current.project === project.name ? h = current.hours : h = 0
              return total + h
            }, 0)
        }
      }).filter(item => item.value > 0)
      return projects
    }
  }
}
</script>

<style scoped>
.my-toolbar {
  margin-top: 1em !important;
}
</style>