view client/src/components/importoverview/ImportOverview.vue @ 4493:b4880e7d4082

client: ImportOverview: change place of save button
author Fadi Abbud <fadi.abbud@intevation.de>
date Fri, 27 Sep 2019 15:49:52 +0200
parents e289d3f32b75
children 8763da6bef4a
line wrap: on
line source

<template>
  <div class="overview">
    <UIBoxHeader
      icon="clipboard-check"
      :title="importReviewLabel"
      :closeCallback="$parent.close"
      :actions="[
        { callback: saveImportsView, icon: 'download' },
        { callback: loadUpdatedLogs, icon: 'sync' }
      ]"
    />
    <div class="position-relative">
      <UISpinnerOverlay v-if="loading" />
      <div class="border-bottom p-2 d-flex justify-content-between">
        <Filters></Filters>
        <button
          class="btn btn-xs btn-info"
          :disabled="!reviewed.length"
          @click="save"
        >
          <translate>Commit</translate> {{ reviewed.length }}
        </button>
      </div>
      <div
        class="p-2 d-flex align-items-center justify-content-between border-bottom"
      >
        <button
          :disabled="!this.prev"
          @click="earlier"
          class="btn btn-xs btn-outline-secondary"
        >
          <font-awesome-icon icon="angle-left" fixed-width />
          <translate>Earlier</translate>
        </button>
        <div class="d-flex align-items-center small">
          {{ interval[0] | dateTime(selectedInterval !== $options.LAST_HOUR) }}
          <template v-if="selectedInterval !== $options.TODAY">
            <span class="mx-2">&ndash;</span>
            {{
              interval[1] | dateTime(selectedInterval !== $options.LAST_HOUR)
            }}
          </template>
          <select
            style="width: 75px; height: 24px"
            class="form-control form-control-sm small ml-2"
            v-model="selectedInterval"
          >
            <option :value="$options.LAST_HOUR">
              <translate>Hour</translate>
            </option>
            <option :value="$options.TODAY"><translate>Day</translate></option>
            <option :value="$options.LAST_7_DAYS">
              <translate>7 days</translate>
            </option>
            <option :value="$options.LAST_30_DAYS">
              <translate>30 Days</translate>
            </option>
          </select>
        </div>
        <div class="btn-group">
          <button
            :disabled="!this.next"
            @click="later"
            class="btn btn-xs btn-outline-secondary"
          >
            <translate>Later</translate>
            <font-awesome-icon icon="angle-right" fixed-width />
          </button>
          <button
            :disabled="!this.next"
            @click="now"
            class="btn btn-xs btn-outline-secondary"
          >
            <font-awesome-icon icon="angle-double-right" fixed-width />
          </button>
        </div>
      </div>
      <UITableHeader
        :columns="[
          { id: 'id', title: `${idLabel}`, width: '75px' },
          { id: 'kind', title: `${kindLabel}`, width: '53px' },
          { id: 'enqueued', title: `${enqueuedLabel}`, width: '138px' },
          { id: 'user', title: `${ownerLabel}`, width: '80px' },
          { id: 'country', title: `${countryLabel}`, width: '55px' },
          { id: 'signer', title: `${signerLabel}`, width: '80px' },
          { id: 'state', title: `${statusLabel}`, width: '72px' },
          { id: 'warnings', icon: 'exclamation-triangle', width: '44px' }
        ]"
      />
      <!--
      For server-side sorting, etc simply don't use the sortTable filter.
      Instead you could just pass a function that loads the imports, like:
      :data="loadImports(sortColumn, sortDirection)"
     -->
      <UITableBody
        :data="filteredImports() | sortTable(sortColumn, sortDirection)"
        :isActive="item => item.id === this.show"
        maxHeight="70vh"
      >
        <template v-slot:row="{ item: entry }">
          <LogEntry :entry="entry"></LogEntry>
        </template>
        <template v-slot:expand="{ item: entry }">
          <LogDetail :entry="entry"></LogDetail>
        </template>
      </UITableBody>
    </div>
  </div>
</template>

<style lang="sass" scoped>
.spinner-overlay
  top: 110px
</style>

<script>
/* This is Free Software under GNU Affero General Public License v >= 3.0
 * without warranty, see README.md and license for details.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * License-Filename: LICENSES/AGPL-3.0.txt
 *
 * Copyright (C) 2018 by via donau
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * Thomas Junk <thomas.junk@intevation.de>
 * Markus Kottländer <markus.kottlaender@intevation.de>
 */

import { mapState, mapGetters } from "vuex";
import { displayError, displayInfo } from "@/lib/errors";
import { STATES } from "@/store/imports";
import { sortTable } from "@/lib/mixins";
import { HTTP } from "@/lib/http";
import {
  startOfDay,
  startOfHour,
  endOfHour,
  endOfDay,
  addDays,
  subDays,
  format
} from "date-fns";

export default {
  components: {
    Filters: () => import("./Filters"),
    LogEntry: () => import("./LogEntry"),
    LogDetail: () => import("./LogDetail")
  },
  mixins: [sortTable],
  LAST_HOUR: "lasthour",
  TODAY: "today",
  LAST_7_DAYS: "lastsevendays",
  LAST_30_DAYS: "lastthirtydays",
  data() {
    return {
      loading: false,
      selectedInterval: this.$options.LAST_HOUR
    };
  },
  computed: {
    ...mapState("application", ["searchQuery"]),
    ...mapState("imports", [
      "show",
      "imports",
      "reviewed",
      "startDate",
      "endDate",
      "prev",
      "next"
    ]),
    ...mapGetters("usermanagement", ["userCountries"]),
    ...mapGetters("imports", ["filters"]),
    countryLabel() {
      return this.$gettext("Country");
    },
    importReviewLabel() {
      return this.$gettext("Import review");
    },
    idLabel() {
      return this.$gettext("Id");
    },
    kindLabel() {
      return this.$gettext("Kind");
    },
    enqueuedLabel() {
      return this.$gettext("Enqueued");
    },
    ownerLabel() {
      return this.$gettext("Owner");
    },
    signerLabel() {
      return this.$gettext("Signer");
    },
    statusLabel() {
      return this.$gettext("Status");
    },
    interval() {
      return [this.startDate, this.endDate];
    }
  },
  watch: {
    $route() {
      const { id } = this.$route.params;
      if (id) this.showSingleRessource(id);
    },
    selectedInterval() {
      this.loadUpdatedLogs();
    },
    imports() {
      if (this.imports.length == 0) {
        if (this.next) {
          const [start, end] = this.determineInterval(this.next);
          this.$store.commit("imports/setStartDate", start);
          this.$store.commit("imports/setEndDate", end);
          this.loadLogs();
        } else if (this.prev) {
          const [start, end] = this.determineInterval(this.prev);
          this.$store.commit("imports/setStartDate", start);
          this.$store.commit("imports/setEndDate", end);
          this.loadLogs();
        }
      }
    },
    filters() {
      this.loadLogs();
    }
  },
  methods: {
    saveImportsView() {
      let element = document.createElement("a");
      element.setAttribute("download", "log.txt");
      element.setAttribute("href", this.csvLink());
      element.click();
    },
    csvLink() {
      return (
        "data:text/csv;charset=utf-8," +
        encodeURIComponent(
          this.imports
            .map(el => {
              return ` ${el.id}, ${el.kind}, ${el.enqueued}, ${el.user ||
                " "}, ${el.country || " "}, ${el.signer || " "}, ${el.state ||
                " "}, ${el.warnings || " "}`;
            })
            .join("\n")
        )
      );
    },
    showSingleRessource(id) {
      id = id * 1;
      this.loadDetails(id)
        .then(response => {
          this.$store.commit("imports/setCurrentDetails", response.data);
          const { enqueued } = response.data;
          this.$store.commit("imports/setStartDate", startOfHour(enqueued));
          this.$store.commit("imports/setEndDate", endOfHour(enqueued));
          this.$store.commit("imports/showDetailsFor", id);
          this.loadLogs();
        })
        .catch(error => {
          this.loading = false;
          this.$store.commit("imports/setCurrentDetails", {});
          const { status, data } = error.response;
          displayError({
            title: this.$gettext("Backend Error"),
            message: `${status}: ${data.message || data}`
          });
        });
    },
    loadDetails(id) {
      return new Promise((resolve, reject) => {
        HTTP.get("/imports/" + id, {
          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
        })
          .then(response => {
            resolve(response);
          })
          .catch(error => {
            reject(error);
          });
      });
    },
    determineInterval(pointInTime) {
      let start, end;
      switch (this.selectedInterval) {
        case this.$options.LAST_HOUR:
          start = startOfHour(pointInTime);
          end = endOfHour(pointInTime);
          break;
        case this.$options.TODAY:
          start = startOfDay(pointInTime);
          end = endOfDay(pointInTime);
          break;
        case this.$options.LAST_7_DAYS:
          start = startOfDay(pointInTime);
          end = endOfDay(addDays(start, 7));
          break;
        case this.$options.LAST_30_DAYS:
          start = startOfDay(pointInTime);
          end = endOfDay(addDays(start, 30));
          break;
      }
      return [start, end];
    },
    earlier() {
      if (!this.prev) return;
      const [start, end] = this.determineInterval(this.prev);
      this.$store.commit("imports/setStartDate", start);
      this.$store.commit("imports/setEndDate", end);
      this.loadLogs();
    },
    later() {
      if (!this.next) return;
      const [start, end] = this.determineInterval(this.next);
      this.$store.commit("imports/setStartDate", start);
      this.$store.commit("imports/setEndDate", end);
      this.loadLogs();
    },
    now() {
      if (!this.next) return;
      const [start, end] = this.determineInterval(new Date());
      this.$store.commit("imports/setStartDate", start);
      this.$store.commit("imports/setEndDate", end);
      this.loadLogs();
    },
    filteredImports() {
      return this.imports.map(x => {
        x["country"] = this.userCountries[x.user];
        return x;
      });
    },
    loadUpdatedLogs() {
      const now = new Date();
      switch (this.selectedInterval) {
        case this.$options.LAST_HOUR:
          this.$store.commit("imports/setStartDate", startOfHour(now));
          this.$store.commit("imports/setEndDate", now);
          break;
        case this.$options.TODAY:
          this.$store.commit("imports/setStartDate", startOfDay(now));
          this.$store.commit("imports/setEndDate", now);
          break;
        case this.$options.LAST_7_DAYS:
          this.$store.commit(
            "imports/setStartDate",
            subDays(startOfDay(now), 7)
          );
          this.$store.commit("imports/setEndDate", now);
          break;
        case this.$options.LAST_30_DAYS:
          this.$store.commit(
            "imports/setStartDate",
            subDays(startOfDay(now), 30)
          );
          this.$store.commit("imports/setEndDate", now);
          break;
      }
      this.loadLogs();
    },
    loadLogs() {
      this.loading = true;
      this.$store
        .dispatch("imports/getImports", {
          filter: this.filters,
          from: encodeURIComponent(
            format(this.startDate, "YYYY-MM-DDTHH:mm:ssZ")
          ),
          to: encodeURIComponent(format(this.endDate, "YYYY-MM-DDTHH:mm:ssZ")),
          query: this.searchQuery
        })
        .then(() => {
          if (this.show) {
            this.loadDetails(this.show)
              .then(response => {
                this.$store.commit("imports/setCurrentDetails", response.data);
                this.loading = false;
              })
              .catch(error => {
                this.loading = false;
                this.$store.commit("imports/setCurrentDetails", {});
                const { status, data } = error.response;
                displayError({
                  title: this.$gettext("Backend Error"),
                  message: `${status}: ${data.message || data}`
                });
              });
          } else {
            this.loading = false;
          }
        })
        .catch(error => {
          const { status, data } = error.response;
          this.loading = false;
          displayError({
            title: this.$gettext("Backend Error"),
            message: `${status}: ${data.message || data}`
          });
        });
    },
    save() {
      if (!this.reviewed.length) return;

      let popupContent = `<table class="table table-sm small mb-0 border-0" style="margin-top: -1px;">`;
      this.reviewed.forEach(r => {
        let imp = this.imports.find(i => i.id === r.id);
        let approved = STATES.APPROVED === r.status;
        popupContent += `<tr>
          <td>${imp.id}</td>
          <td>${imp.kind.toUpperCase()}</td>
          <td>${this.$options.filters.dateTime(imp.enqueued)}</td>
          <td class="text-${approved ? "success" : "danger"}">
            ${this.$gettext(approved ? "approved" : "declined")}
          </td>
        </tr>`;
      });
      popupContent += "</table>";

      this.$store.commit("application/popup", {
        icon: "clipboard-check",
        title: this.$gettext("Finish Review"),
        padding: false,
        big: true,
        content: popupContent,
        confirm: {
          icon: "check",
          callback: () => {
            let data = this.reviewed.map(r => ({
              id: r.id,
              state: r.status
            }));
            this.$store
              .dispatch("imports/confirmReview", data)
              .then(response => {
                this.loadLogs();
                this.$store.commit("imports/setReviewed", []);
                this.$store.commit("map/startRefreshLayers");
                this.$store.commit("gauges/deleteNashSutcliffeCache");
                this.$store.dispatch("map/refreshLayers");
                this.$store.dispatch("imports/loadStagingNotifications");
                this.$store.dispatch("imports/loadStretches");
                this.$store.dispatch("imports/loadSections");
                this.$store.commit("map/reviewActive", false);
                this.$nextTick(() => {
                  this.$store.commit("map/finishRefreshLayers");
                });
                const messages = response.data
                  .map(x => {
                    if (x.message) return x.message;
                    if (x.error) return x.error;
                  })
                  .join("\n\n");
                displayInfo({
                  title: "Staging Area",
                  message: messages,
                  options: {
                    timeout: 0,
                    buttons: [{ text: "Ok", action: null, bold: true }]
                  }
                });
              })
              .catch(error => {
                const { status, data } = error.response;
                displayError({
                  title: "Backend Error",
                  message: `${status}: ${data.message || data}`
                });
              });
          }
        },
        cancel: {
          label: this.$gettext("Cancel"),
          icon: "times"
        }
      });
    }
  },
  mounted() {
    this.loadUpdatedLogs();
    this.$store.dispatch("usermanagement/loadUsers").catch(error => {
      const { status, data } = error.response;
      displayError({
        title: this.$gettext("Backend Error"),
        message: `${status}: ${data.message || data}`
      });
    });
    const { id } = this.$route.params;
    if (id) {
      this.showSingleRessource(id);
    } else {
      this.$store.commit("application/searchQuery", "");
      this.loadLogs();
    }
  },
  activated() {
    this.loadUpdatedLogs();
  }
};
</script>