repos
/ timestrap-django master

timestrap-django

mirror archived upstream

Time tracking you can host anywhere, built on Django. Full export support in multiple formats and easily extensible.

djangodockerhandcodedpythonself-hostedtime-trackingtimetracker

11.7 KB · 426 lines · vue Raw History
  1<template>
  2<div class="container-fluid">
  3  <div class="row row-background">
  4    <div class="col-md-7">
  5      <h1 class="row-title">
  6        <icon :icon="['fas', 'book']" class="fa-sm mr-2"/>
  7        Reports
  8      </h1>
  9    </div>
 10    <div class="col-md-5 d-flex align-items-center justify-content-end">
 11      <template v-if="is_staff">
 12        <select
 13          v-model="exportFormat"
 14          class="export-select custom-select form-control-sm w-25 flex-fill mr-2">
 15          <option value="csv">csv</option>
 16          <option value="xls">xls</option>
 17          <option value="tsv">tsv</option>
 18          <option value="ods">ods</option>
 19          <option value="json">json</option>
 20          <option value="yaml">yaml</option>
 21          <option value="html">html</option>
 22        </select>
 23        <button
 24          id="export-report"
 25          class="btn btn-light flex-fill text-nowrap mr-2"
 26          @click="exportReport">
 27          <icon :icon="['fas', 'download']" class="mr-1"/>
 28          Export
 29        </button>
 30      </template>
 31
 32      <popover class="flex-fill mr-2">
 33        <template slot="popover-button">
 34          <span  v-bind:class="{active: isFiltered}">
 35            <icon :icon="['fas', 'filter']" class="mr-1"/>
 36            Filter
 37          </span>
 38        </template>
 39        <template slot="popover-content">
 40          <small class="text-muted mb-2 d-block">Filter by column, multiple filters are okay</small>
 41          <div class="row">
 42            <div class="col-sm-6">
 43              <div class="form-group">
 44                <label>User</label>
 45                <selector
 46                  id="report-filter-user"
 47                  v-model="user"
 48                  :options="users"
 49                  :selected="user"
 50                  placeholder="User"
 51                  allowclear/>
 52              </div>
 53              <div class="form-group">
 54                <label>Client</label>
 55                <selector
 56                  id="report-filter-client"
 57                  v-model="client"
 58                  :options="clients"
 59                  :selected="client"
 60                  placeholder="Client"
 61                  allowclear/>
 62              </div>
 63              <div class="form-group mb-0">
 64                <label>Project</label>
 65                <selector
 66                  id="report-filter-project"
 67                  v-model="project"
 68                  :options="projects"
 69                  :selected="project"
 70                  placeholder="Project"
 71                  allowclear/>
 72              </div>
 73            </div>
 74            <div class="col-sm-6">
 75              <div class="form-group">
 76                <label>Task</label>
 77                <selector
 78                  id="report-filter-task"
 79                  v-model="task"
 80                  :options="tasks"
 81                  :selected="task"
 82                  placeholder="Task"
 83                  allowclear/>
 84              </div>
 85              <div class="form-group">
 86                <label>Min. Date</label>
 87                <datepicker
 88                  id="report-filter-min-date"
 89                  v-model="minDate"
 90                  type="text"
 91                  class="form-control date-input"
 92                  placeholder="Min. date"
 93                  allowclear/>
 94              </div>
 95              <div class="form-group mb-0">
 96                <label>Max. Date</label>
 97                <datepicker
 98                  id="report-filter-max-date"
 99                  v-model="maxDate"
100                  type="text"
101                  class="form-control date-input"
102                  placeholder="Max. date"
103                  allowclear/>
104              </div>
105            </div>
106
107            <div class="clear col-sm-12">
108              <button
109                class="btn btn-info w-100 clear"
110                @click.prevent.exact="clear">
111                Clear
112              </button>
113            </div>
114
115          </div>
116        </template>
117      </popover>
118
119      <popover class="flex-fill">
120        <template slot="popover-button">
121          <icon :icon="['fas', 'sort']" class="mr-1"/>
122          Sort
123        </template>
124        <template slot="popover-content">
125          <small class="text-muted mb-2 d-block">Sort by column and direction</small>
126          <div class="row">
127            <div class="col-md-6">
128              <selector
129                id="report-sort-by"
130                v-model="orderBy"
131                :options="orderByOptions"
132                selected="date"
133                placeholder="Order by"/>
134            </div>
135            <div class="col-md-6">
136              <selector
137                id="report-order-dir"
138                v-model="orderDir"
139                :options="orderDirOptions"
140                selected="desc"
141                placeholder="Order direction"/>
142            </div>
143          </div>
144        </template>
145      </popover>
146    </div>
147  </div>
148
149  <template v-if="entries.length">
150    <div class="row">
151      <table class="table table-striped table-responsive-md table-sm small">
152        <thead class="thead-dark">
153          <tr>
154            <th scope="col">
155              <icon :icon="['fas', 'address-book']" class="mr-1"/>
156              Client
157            </th>
158            <th scope="col">
159              <icon :icon="['fas', 'briefcase']" class="mr-1"/>
160              Project
161            </th>
162            <th scope="col">
163              <icon :icon="['fas', 'tasks']" class="mr-1"/>
164              Task
165            </th>
166            <th scope="col">
167              <icon :icon="['fas', 'user-circle']" class="mr-1"/>
168              User
169            </th>
170            <th scope="col">
171              <icon :icon="['fas', 'calendar']" class="mr-1"/>
172              Date
173            </th>
174            <th scope="col">
175              <icon :icon="['fas', 'clock']" class="mr-1"/>
176              Duration
177            </th>
178            <th scope="col">
179              <icon :icon="['fas', 'comment']" class="mr-1"/>
180              Note
181            </th>
182          </tr>
183        </thead>
184        <tbody>
185          <tr
186            v-for="entry in entries"
187            :key="entry.id"
188            :id="'entry-' + entry.id">
189            <td>{{ entry.project ? getClient(getProject(entry.project).client).name : '' }}</td>
190            <td>{{ entry.project ? getProject(entry.project).name : '' }}</td>
191            <td>{{ entry.task ? getTask(entry.task).name : '' }}</td>
192            <td>{{ entry.user ? getUser(entry.user).username : '' }}</td>
193            <td>{{ $moment(entry.date).format('LL') }}</td>
194            <td class="text-right">{{ $moment.duration(entry.duration, 'hours').format('h[h] mm[m]') }}</td>
195            <td class="entry__note">{{ entry.note }}</td>
196          </tr>
197        </tbody>
198        <tfoot>
199          <tr>
200            <td>
201              <small class="text-muted text-uppercase">Count</small>
202              {{ entries.length }}
203            </td>
204            <td></td>
205            <td></td>
206            <td></td>
207            <td></td>
208            <td class="text-right text-nowrap">
209              <small class="text-muted text-uppercase">Sum</small>
210              {{ $moment.duration(total, 'hours').format('d[d] h[h] m[m]') }}
211            </td>
212            <td></td>
213          </tr>
214        </tfoot>
215      </table>
216    </div>
217  </template>
218  <div v-else class="container">
219    <div class="row">
220      <div class="col text-center py-4">
221        <div class="spinner-border" role="status">
222          <span class="sr-only">Loading...</span>
223        </div>
224      </div>
225    </div>
226  </div>
227</div>
228</template>
229
230
231<script>
232import {mapGetters, mapState, mapActions, mapMutations} from 'vuex';
233import $ from 'jquery';
234
235import Datepicker from '../datepicker.vue';
236import Popover from '../popover.vue';
237import Entry from '../entry.vue';
238import Selector from '../selector.vue';
239
240
241export default {
242  components: {
243    Datepicker,
244    Popover,
245    Entry,
246    Selector,
247  },
248  data() {
249    return {
250      is_staff: timestrapConfig.USER.IS_STAFF,
251      editable: false,
252      next: null,
253      previous: null,
254
255      exportFormat: 'csv',
256
257      orderByOptions: [
258        {id: 'project__client__name', text: 'Client'},
259        {id: 'date', text: 'Date'},
260        {id: 'project__name', text: 'Project'},
261        {id: 'task__name', text: 'Task'},
262        {id: 'user__username', text: 'User'},
263      ],
264      orderDirOptions: [
265        {id: 'asc', text: 'Ascending'},
266        {id: 'desc', text: 'Descending'},
267      ],
268
269      orderBy: 'date',
270      orderDir: 'desc',
271
272      minDate: null,
273      maxDate: null,
274      user: null,
275      client: this.$store.state.reports.client,
276      project: this.$store.state.reports.project,
277      task: this.$store.state.reports.task,
278    };
279  },
280  watch: {
281    orderBy() {
282      this.setOrderBy(this.orderBy);
283      this.$store.dispatch('reports/getReport');
284    },
285    orderDir() {
286      this.setOrderDir(this.orderDir);
287      this.$store.dispatch('reports/getReport');
288    },
289    user() {
290      this.setUser(this.user);
291      this.$store.dispatch('reports/getReport');
292    },
293    client() {
294      this.setClient(this.client);
295      this.$store.dispatch('reports/getReport');
296    },
297    project() {
298      this.setProject(this.project);
299      this.$store.dispatch('reports/getReport');
300    },
301    task() {
302      this.setTask(this.task);
303      this.$store.dispatch('reports/getReport');
304    },
305    minDate() {
306      this.setMinDate(this.minDate);
307      this.$store.dispatch('reports/getReport');
308    },
309    maxDate() {
310      this.setMaxDate(this.maxDate);
311      this.$store.dispatch('reports/getReport');
312    },
313  },
314  computed: {
315    ...mapState({
316      entries: state => state.reports.all,
317      total: state => state.reports.total,
318      subtotal: state => state.reports.subtotal,
319    }),
320    ...mapGetters({
321      tasks: 'tasks/getSelectTasks',
322      projects: 'projects/getSelectProjects',
323      clients: 'clients/getSelectClients',
324      users: 'users/getSelectUsers',
325      getTask: 'tasks/getTask',
326      getProject: 'projects/getProject',
327      getClient: 'clients/getClient',
328      getUser: 'users/getUser',
329      isFiltered: 'reports/isFiltered'
330    }),
331  },
332  mounted() {
333    this.$store.dispatch('reports/getReport');
334  },
335  methods: {
336    ...mapMutations('reports', [
337      'setOrderBy',
338      'setOrderDir',
339
340      'setUser',
341      'setClient',
342      'setProject',
343      'setTask',
344      'setMinDate',
345      'setMaxDate',
346    ]),
347    clear(){
348      this.minDate = null;
349      this.maxDate = null;
350      this.user = null;
351      this.client = null;
352      this.project = null;
353      this.task = null;
354    },
355    exportReport() {
356      let ordering = (this.orderDir === 'desc' ? '-' : '') + this.orderBy;
357      if (this.orderBy !== 'date') {
358        ordering += ',-date';
359      }
360
361      function urlToId(url) {
362        if (url !== null) {
363          let splitUrl = url.split('/');
364          return splitUrl[splitUrl.length - 2];
365        } else {
366          return "";
367        }
368      }
369
370      const query = {
371        ordering: ordering,
372        user: urlToId(this.user),
373        project: urlToId(this.project),
374        project__client: urlToId(this.client),
375        task: urlToId(this.task),
376        min_date: this.minDate,
377        max_date: this.maxDate,
378        export_format: this.exportFormat,
379      };
380      document.location.href = timestrapConfig.CORE_URLS.REPORTS_EXPORT + '?' + $.param(query);
381    },
382  },
383};
384</script>
385
386
387<style lang="scss" scoped>
388.row-background {
389  background-image: url('/static/imgs/background.jpg');
390  background-size: cover;
391  background-position: center center;
392  padding-top: 3rem;
393  padding-bottom: 3rem;
394}
395
396.row-title {
397  margin-bottom: 0;
398  color: #fff;
399  text-shadow: 1px 1px 0 #000;
400  font-weight: bold;
401
402  .fa-sm {
403    filter: drop-shadow(1px 1px 0 #000);
404  }
405}
406
407.entry__note {
408  max-width: 20vw;
409  white-space: nowrap;
410  overflow: hidden;
411  text-overflow: ellipsis;
412}
413
414.clear{
415  margin-top: .5rem;
416}
417
418.active{
419  /*svg{*/
420  /*  color: green;*/
421  /*}*/
422  font-weight: bold;
423}
424
425</style>