view client/src/components/importoverview/ImportOverview.vue @ 5095:e21cbb9768a2

Prevent duplicate fairway areas In principal, there can be only one or no fairway area at each point on the map. Since polygons from real data will often be topologically inexact, just disallow equal geometries. This will also help to avoid importing duplicates with concurrent imports, once the history of fairway dimensions will be preserved.
author Tom Gottfried <tom@intevation.de>
date Wed, 25 Mar 2020 18:10:02 +0100
parents 9f0830a1845d
children 03f28c5d2a88
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: '70px' },
          {
            id: 'kind',
            title: `${kindLabel.replace('fm_').toUpperCase()}`,
            width: '125px'
          },
          { id: 'enqueued', title: `${enqueuedLabel}`, width: '135px' },
          { id: 'user', title: `${ownerLabel}`, width: '80px' },
          { id: 'country', title: `${countryLabel}`, width: '50px' },
          { id: 'signer', title: `${signerLabel}`, width: '80px' },
          { id: 'state', title: `${statusLabel}`, width: '72px' },
          { id: 'changed', title: `${changedLabel}`, width: '138px' },
          { 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 app from "@/main";
import { saveAs } from "file-saver";
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");
    },
    changedLabel() {
      return this.$gettext("Changed");
    },
    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() {
      const content = "";
      this.$store.commit("application/popup", {
        icon: "clipboard-check",
        title: this.$gettext("Export logs"),
        padding: false,
        big: true,
        content: content,
        confirm: {
          icon: "check",
          callback: dates => {
            const { from, to } = dates;
            displayInfo({
              title: "Generating CSV",
              message: `${from} - ${to}`,
              options: {
                timeout: 0,
                buttons: [{ text: "Ok", action: null, bold: true }]
              }
            });
            HTTP.get(
              `/imports/export?from=${encodeURIComponent(
                format(startOfDay(new Date(from)), "YYYY-MM-DDTHH:mm:ssZ")
              )}&to=${encodeURIComponent(
                format(endOfDay(new Date(to)), "YYYY-MM-DDTHH:mm:ssZ")
              )}&query=`,
              {
                headers: { "X-Gemma-Auth": localStorage.getItem("token") }
              }
            )
              .then(response => {
                const imports = response.data;
                app.$snotify.clear();
                if (!imports) return;
                const csvFile = new Blob([new TextEncoder().encode(imports)], {
                  type: "text/csv"
                });
                saveAs(csvFile, "log.csv");
              })
              .catch(error => {
                console.log(error);
                const { status, data } = error.response;
                app.$snotify.clear();
                displayError({
                  title: this.$gettext("Backend Error"),
                  message: `${status}: ${data.message || data}`
                });
              });
          }
        },
        cancel: {
          label: this.$gettext("Cancel"),
          icon: "times"
        },
        dateSelection: true
      });
    },
    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 => {
          this.loading = false;
          let message = "Backend not reachable";
          if (error.response) {
            const { status, data } = error.response;
            message = `${status}: ${data.message || data}`;
          }
          displayError({
            title: this.$gettext("Backend Error"),
            message: message
          });
        });
    },
    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 approved = STATES.APPROVED === r.status;
        popupContent += `<tr>
          <td>${r.id}</td>
          <td>${r.kind.toUpperCase()}</td>
          <td>${this.$options.filters.dateTime(r.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 => {
                let message = "Backend not reachable";
                if (error.response) {
                  const { status, data } = error.response;
                  message = `${status}: ${data.message || data}`;
                }
                displayError({
                  title: this.$gettext("Backend Error"),
                  message: message
                });
              });
          }
        },
        cancel: {
          label: this.$gettext("Cancel"),
          icon: "times"
        }
      });
    }
  },
  mounted() {
    this.loadUpdatedLogs();
    this.$store.dispatch("usermanagement/loadUsers").catch(error => {
      let message = "Backend not reachable";
      if (error.response) {
        const { status, data } = error.response;
        message = `${status}: ${data.message || data}`;
      }
      displayError({
        title: this.$gettext("Backend Error"),
        message: message
      });
    });
    const { id } = this.$route.params;
    if (id) {
      this.showSingleRessource(id);
    } else {
      this.$store.commit("application/searchQuery", "");
      this.loadLogs();
    }
  },
  activated() {
    this.loadUpdatedLogs();
  }
};
</script>