Skip to content
Snippets Groups Projects
Feature_list.vue 23.6 KiB
Newer Older
<template>
  <div class="fourteen wide column">
Timothee P's avatar
Timothee P committed
    <script
      type="application/javascript"
      :src="
        baseUrl +
        '/resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js'
      "
    ></script>
    <div class="feature-list-container ui grid">
      <div class="four wide column">
        <h1>Signalements</h1>
      </div>
      <div class="twelve wide column">
        <div class="ui dimmer" :class="[{ active: featureLoading }]">
          <div class="ui large text loader">Chargement</div>
        </div>
          <a
            @click="showMap = true"
            :class="['item', { active: showMap }]"
            data-tab="map"
            data-tooltip="Carte"
          <a
            @click="showMap = false"
            :class="['item', { active: !showMap }]"
            data-tab="list"
            data-tooltip="Liste"
            ><i class="list fitted icon"></i
          ></a>
          <div class="item">
            <h4>
              {{ filteredFeatures.length }} signalement{{
                filteredFeatures.length > 1 ? "s" : ""
Timothee P's avatar
Timothee P committed

          <div
            v-if="project && feature_types && permissions.can_create_feature"
            class="item right"
          >
              @click="showAddFeature = !showAddFeature"
              class="
                ui
                dropdown
                top
                right
                pointing
                compact
                button button-hover-green
              "
              data-tooltip="Ajouter un signalement"
              data-position="bottom left"
            >
              <i class="plus fitted icon"></i>
                v-if="showAddFeature"
                class="menu transition visible"
                style="z-index: 9999"
              >
                <div class="header">Ajouter un signalement du type</div>
                <div class="scrolling menu text-wrap">
                  <router-link
                    :to="{
                      name: 'ajouter-signalement',
DESPRES Damien's avatar
DESPRES Damien committed
                      params: { slug_type_signal: type.slug },
                    v-for="(type, index) in feature_types"
                    :key="type.slug + index"
Timothee P's avatar
Timothee P committed

leandro's avatar
leandro committed
            <div v-if="project && feature_types" class="item right">
Timothee P's avatar
Timothee P committed
              <div
                v-if="checkedFeatures.length"
                class="ui top center pointing compact button button-hover-red"
                data-tooltip="Effacer tous les types de signalements sélectionnés"
                data-position="left center"
                data-variation="mini"
leandro's avatar
leandro committed
              >
                <i class="grey trash icon" @click="modalAllDelete"></i>
Timothee P's avatar
Timothee P committed
              </div>
leandro's avatar
leandro committed
            </div>
          </div>
        </div>
      </div>
    </div>

    <form id="form-filters" class="ui form grid" action="" method="get">
      <div class="field wide four column">
        <label>Type</label>
DESPRES Damien's avatar
DESPRES Damien committed
          @update:selection="onFilterTypeChange($event)"
          :options="form.type.choices"
          :selected="form.type.selected"
          :selection.sync="form.type.selected"
      </div>
      <div class="field wide four column">
        <label>Statut</label>
        <!--  //* giving an object mapped on key name -->
DESPRES Damien's avatar
DESPRES Damien committed
          @update:selection="onFilterStatusChange($event)"
          :options="form.status.choices"
          :selected="form.status.selected.name"
          :selection.sync="form.status.selected"
      </div>
      <div class="field wide four column">
        <label>Nom</label>
        <div class="ui icon input">
          <i class="search icon"></i>
          <div class="ui action input">
Timothee P's avatar
Timothee P committed
            <input
              type="text"
              name="title"
              v-model="form.title"
              @input="onFilterChange()"
            />
            <button
              type="button"
              class="ui teal icon button"
              id="submit-search"
            >
              <i class="search icon"></i>
            </button>
          </div>
        </div>
      </div>
Timothee P's avatar
Timothee P committed
      <!-- map params, updated on map move // todo : brancher sur la carte probablement -->
DESPRES Damien's avatar
DESPRES Damien committed
      <input type="hidden" name="zoom" v-model="zoom" />
DESPRES Damien's avatar
DESPRES Damien committed
      <input type="hidden" name="lat" v-model="lat" />
      <input type="hidden" name="lng" v-model="lng" />
    <div v-show="showMap" class="ui tab active map-container" data-tab="map">
Timothee P's avatar
Timothee P committed
      <SidebarLayers v-if="baseMaps && map" />
    <div v-show="!showMap" data-tab="list" class="dataTables_wrapper no-footer">
      <table id="table-features" class="ui compact table">
        <thead>
          <tr>
Timothee P's avatar
Timothee P committed
            <th class="center"></th>

leandro's avatar
leandro committed
            <th class="center">
Timothee P's avatar
Timothee P committed
              Statut
              <i
                :class="{
                  down: isSortedAsc('statut'),
                  up: isSortedDesc('statut'),
                }"
                class="icon sort"
                @click="changeSort('statut')"
              />
            </th>
            <th class="center">
              Type
              <i
                :class="{ down: isSortedAsc('type'), up: isSortedDesc('type') }"
                class="icon sort"
                @click="changeSort('type')"
              />
            </th>
            <th class="center">
              Nom
              <i
                :class="{ down: isSortedAsc('nom'), up: isSortedDesc('nom') }"
                class="icon sort"
                @click="changeSort('nom')"
              />
            </th>
            <th class="center">
              Dernière modification
              <i
                :class="{
                  down: isSortedAsc('updated_on'),
                  up: isSortedDesc('updated_on'),
                }"
                class="icon sort"
                @click="changeSort('updated_on')"
              />
            </th>
            <th class="center" v-if="user">
              Auteur
              <i
                :class="{
                  down: isSortedAsc('display_creator'),
                  up: isSortedDesc('display_creator'),
                }"
                class="icon sort"
                @click="changeSort('display_creator')"
              />
leandro's avatar
leandro committed
            </th>
          <tr v-for="(feature, index) in paginatedFeatures" :key="index">
leandro's avatar
leandro committed
            <td class="center">
Timothee P's avatar
Timothee P committed
              <div class="ui checkbox">
                <input
                  type="checkbox"
                  :id="feature.id"
                  :value="feature.id"
                  v-model="checkedFeatures"
                  :checked="checkedFeatures[feature.id]"
                />
                <label></label>
              </div>
            </td>
leandro's avatar
leandro committed

DESPRES Damien's avatar
DESPRES Damien committed
            <td class="center">
Timothee P's avatar
Timothee P committed
              <div
                v-if="feature.properties.status.value == 'archived'"
                data-tooltip="Archivé"
              >
                <i class="grey archive icon"></i>
              </div>
              <div
DESPRES Damien's avatar
DESPRES Damien committed
                v-else-if="feature.properties.status.value == 'pending'"
                data-tooltip="En attente de publication"
              >
                <i class="teal hourglass outline icon"></i>
              </div>
              <div
DESPRES Damien's avatar
DESPRES Damien committed
                v-else-if="feature.properties.status.value == 'published'"
                data-tooltip="Publié"
              >
                <i class="olive check icon"></i>
              </div>
              <div
DESPRES Damien's avatar
DESPRES Damien committed
                v-else-if="feature.properties.status.value == 'draft'"
                data-tooltip="Brouillon"
              >
                <i class="orange pencil alternate icon"></i>
              </div>
            </td>
DESPRES Damien's avatar
DESPRES Damien committed
            <td class="center">
              <router-link
                :to="{
                  name: 'details-type-signalement',
Timothee P's avatar
Timothee P committed
                  params: {
                    feature_type_slug: feature.properties.feature_type.title,
                  },
DESPRES Damien's avatar
DESPRES Damien committed
                {{ feature.properties.feature_type.title }}
DESPRES Damien's avatar
DESPRES Damien committed
            <td class="center">
              <router-link
                :to="{
                  name: 'details-signalement',
                  params: {
DESPRES Damien's avatar
DESPRES Damien committed
                    slug_type_signal: feature.properties.feature_type.slug,
                    slug_signal: feature.properties.slug || feature.id,
Timothee P's avatar
Timothee P committed
                >{{ getFeatureDisplayName(feature) }}</router-link
DESPRES Damien's avatar
DESPRES Damien committed
            <td class="center">
DESPRES Damien's avatar
DESPRES Damien committed
              {{ feature.properties.updated_on }}
DESPRES Damien's avatar
DESPRES Damien committed
              {{ feature.properties.creator.username }}
          <tr v-if="filteredFeatures.length === 0" class="odd">
            <td colspan="5" class="dataTables_empty" valign="top">
              Aucune donnée disponible
            </td>
          </tr>
Timothee P's avatar
Timothee P committed
      <div
        class="dataTables_info"
        id="table-features_info"
        role="status"
        aria-live="polite"
      >
        Affichage de l'élément {{ pagination.start + 1 }} à
        {{ pagination.end + 1 }} sur {{ filteredFeatures.length }} éléments
Timothee P's avatar
Timothee P committed
      </div>
DESPRES Damien's avatar
DESPRES Damien committed
      <div
        class="dataTables_paginate paging_simple_numbers"
        id="table-features_paginate"
Timothee P's avatar
Timothee P committed
      >
        <a
          @click="toPreviousPage"
          class="paginate_button previous disabled"
          aria-controls="table-features"
          data-dt-idx="0"
          tabindex="0"
          id="table-features_previous"
          >Précédent</a
DESPRES Damien's avatar
DESPRES Damien committed
        >
            :key="'page' + index"
            :class="[
              'paginate_button',
              { current: page.value === pagination.currentPage },
            ]"
            aria-controls="table-features"
            data-dt-idx="1"
            tabindex="0"
            >{{ page.value }}</a
          >
        </span>
        <!-- // TODO : <span v-if="nbPages > 4" class="ellipsis">...</span> -->
Timothee P's avatar
Timothee P committed
        <a
          class="paginate_button next"
          aria-controls="table-features"
          data-dt-idx="7"
          tabindex="0"
          id="table-features_next"
          @click="toNextPage"
          >Suivant</a
        >
      </div>
leandro's avatar
leandro committed
    <!-- MODAL ALL DELETE FEATURE TYPE -->
    <div
      v-if="modalAllDeleteOpen"
      class="ui dimmer modals page transition visible active"
      style="display: flex !important"
Timothee P's avatar
Timothee P committed
    >
leandro's avatar
leandro committed
      <div
        :class="[
          'ui mini modal subscription',
          { 'active visible': modalAllDeleteOpen },
        ]"
      >
        <i @click="modalAllDeleteOpen = false" class="close icon"></i>
        <div class="ui icon header">
          <i class="trash alternate icon"></i>
Timothee P's avatar
Timothee P committed
          Êtes-vous sûr de vouloir effacer
          <span v-if="checkedFeatures.length == 1"> un signalement ? </span>
          <span v-else> ces {{ checkedFeatures.length }} signalements ? </span>
leandro's avatar
leandro committed
        </div>
        <div class="actions">
Timothee P's avatar
Timothee P committed
          <button
Timothee P's avatar
Timothee P committed
            type="button"
            class="ui red compact fluid button"
          >
            Confirmer la suppression
          </button>
leandro's avatar
leandro committed
        </div>
      </div>
    </div>
import { mapGetters, mapState } from "vuex";
DESPRES Damien's avatar
DESPRES Damien committed
import { mapUtil } from "@/assets/js/map-util.js";
import SidebarLayers from "@/components/map-layers/SidebarLayers";
import Dropdown from "@/components/Dropdown.vue";
DESPRES Damien's avatar
DESPRES Damien committed
const axios = require("axios");

  components: {
    SidebarLayers,
leandro's avatar
leandro committed
      modalAllDeleteOpen: false,
      checkedFeatures: [],
          selected: {
            name: null,
            {
              name: "Brouillon",
              value: "draft",
            },
            {
              name: "En attente de publication",
              value: "pending",
            },
            {
              name: "Publié",
              value: "published",
            },
            {
              name: "Archivé",
              value: "archived",
            },
Timothee P's avatar
Timothee P committed
        title: null,
Timothee P's avatar
Timothee P committed
        pagesize: 15,
Timothee P's avatar
Timothee P committed
      sort: {
        column: "",
        ascending: true,
DESPRES Damien's avatar
DESPRES Damien committed
      },
      geojsonFeatures: [],
      featureLoading: false,
      filterStatus: null,
      filterType: null,
      baseUrl: this.$store.state.configuration.BASE_URL,
      map: null,
      zoom: null,
      lat: null,
      lng: null,
      showAddFeature: false,
Timothee P's avatar
Timothee P committed
    ...mapGetters(["project", "permissions"]),
Timothee P's avatar
Timothee P committed
    ...mapState(["user"]),
    ...mapState("feature", ["features"]),
    ...mapState("feature_type", ["feature_types"]),
Timothee P's avatar
Timothee P committed

    baseMaps() {
DESPRES Damien's avatar
DESPRES Damien committed
      return this.$store.state.map.basemaps;
    },
leandro's avatar
leandro committed
      let N = Math.round(
        this.filteredFeatures.length / this.pagination.pagesize
leandro's avatar
leandro committed
      );
      let rest = Math.round(
        this.filteredFeatures.length % this.pagination.pagesize
leandro's avatar
leandro committed
      );
      if (rest > 0) N++;
      const arr = [...Array(N).keys()].map(function (x) {
leandro's avatar
leandro committed
        ++x;
        return {
          index: x,
leandro's avatar
leandro committed
      return arr;
    },

    filteredFeatures() {
      let results = this.geojsonFeatures;
      if (this.filterType) {
        results = results.filter(
          (el) => el.properties.feature_type.title === this.filterType
        );
      }
      if (this.filterStatus) {
        console.log("filter by" + this.filterStatus);
        results = results.filter(
          (el) => el.properties.status.value === this.filterStatus
        );
      }
      if (this.form.title) {
        results = results.filter((el) => {
          if (el.properties.title) {
            return el.properties.title
              .toLowerCase()
              .includes(this.form.title.toLowerCase());
          } else
            return el.id.toLowerCase().includes(this.form.title.toLowerCase());
leandro's avatar
leandro committed
        });
leandro's avatar
leandro committed
    },

    paginatedFeatures() {
      let filterdFeatures = [...this.filteredFeatures];
DESPRES Damien's avatar
DESPRES Damien committed
      // Ajout du tri
Timothee P's avatar
Timothee P committed
      if (this.sort.column != "") {
        filterdFeatures = filterdFeatures.sort((a, b) => {
          let aProp = this.getFeatureDisplayName(a);
          let bProp = this.getFeatureDisplayName(b);
          if (this.sort.column == "statut") {
            aProp = a.properties.status.value;
            bProp = b.properties.status.value;
          } else if (this.sort.column == "type") {
            aProp = a.properties.feature_type.title;
            bProp = b.properties.feature_type.title;
          } else if (this.sort.column == "updated_on") {
            aProp = a.properties.updated_on;
            bProp = b.properties.updated_on;
          } else if (this.sort.column == "display_creator") {
            aProp = a.properties.display_creator;
            bProp = b.properties.display_creator;
          }
          //ascending
          if (this.sort.ascending) {
            if (aProp < bProp) {
              return -1;
DESPRES Damien's avatar
DESPRES Damien committed
            }
Timothee P's avatar
Timothee P committed
            if (aProp > bProp) {
              return 1;
DESPRES Damien's avatar
DESPRES Damien committed
            }
Timothee P's avatar
Timothee P committed
            return 0;
          } else {
            //descending
            if (aProp < bProp) {
              return 1;
DESPRES Damien's avatar
DESPRES Damien committed
            }
Timothee P's avatar
Timothee P committed
            if (aProp > bProp) {
              return -1;
DESPRES Damien's avatar
DESPRES Damien committed
            }
Timothee P's avatar
Timothee P committed
            return 0;
          }
        });
DESPRES Damien's avatar
DESPRES Damien committed
      }
Timothee P's avatar
Timothee P committed
      return filterdFeatures.slice(this.pagination.start, this.pagination.end);
  },

  methods: {
    modalAllDelete() {
      this.modalAllDeleteOpen = !this.modalAllDeleteOpen;
    },

    deleteFeature(feature) {
      const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/${feature.feature_id}`;
      axios
        .delete(url, {})
        .then(() => {
          if (!this.modalAllDeleteOpen) {
            this.$router.go();
          }
        })
        .catch(() => {
          return false;
        });
    },

    deleteAllFeatureSelection() {
      let feature = {};
      this.checkedFeatures.forEach((feature_id) => {
        feature = { feature_id: feature_id };
        this.deleteFeature(feature);
      });
      this.modalAllDelete();
    },

    getFeatureDisplayName(feature) {
      return feature.properties.title || feature.id;
    },

Timothee P's avatar
Timothee P committed
    isSortedAsc(column) {
      return this.sort.column == column && this.sort.ascending;
DESPRES Damien's avatar
DESPRES Damien committed
    },
Timothee P's avatar
Timothee P committed
    isSortedDesc(column) {
      return this.sort.column == column && !this.sort.ascending;
DESPRES Damien's avatar
DESPRES Damien committed
    },
Timothee P's avatar
Timothee P committed
    changeSort(column) {
      if (this.sort.column == column) {
        //changer order
        this.sort.ascending = !this.sort.ascending;
      } else {
        this.sort.column = column;
        this.sort.ascending = true;
DESPRES Damien's avatar
DESPRES Damien committed
      }
    },
Timothee P's avatar
Timothee P committed
    onFilterStatusChange(newvalue) {
      this.filterStatus = null;
      if (newvalue) {
        console.log("filter change", newvalue.value);
        this.filterStatus = newvalue.value;
DESPRES Damien's avatar
DESPRES Damien committed
      this.onFilterChange();
    },
Timothee P's avatar
Timothee P committed
    onFilterTypeChange(newvalue) {
      this.filterType = null;
      if (newvalue) {
        console.log("filter change", newvalue);
        this.filterType = newvalue;
DESPRES Damien's avatar
DESPRES Damien committed
      }
      this.onFilterChange();
    },

    onFilterChange() {
DESPRES Damien's avatar
DESPRES Damien committed
      this.featureGroup.clearLayers();
      this.featureGroup = mapUtil.addFeatures(features, {});
      if (features.length > 0) {
        mapUtil.getMap().fitBounds(this.featureGroup.getBounds());
    toPreviousPage() {
      if (this.pagination.start > 0) {
DESPRES Damien's avatar
DESPRES Damien committed
        this.pagination.start -= this.pagination.pagesize;
        this.pagination.end -= this.pagination.pagesize;
      if (this.pagination.end < this.filteredFeatures.length) {
DESPRES Damien's avatar
DESPRES Damien committed
        this.pagination.start += this.pagination.pagesize;
        this.pagination.end += this.pagination.pagesize;
Timothee P's avatar
Timothee P committed
    loadFeatures(features) {
DESPRES Damien's avatar
DESPRES Damien committed
      this.geojsonFeatures = features;
      const urlParams = new URLSearchParams(window.location.search);
      const featureType = urlParams.get("feature_type");
      const featureStatus = urlParams.get("status");
      const featureTitle = urlParams.get("title");
      this.featureGroup = mapUtil.addFeatures(this.geojsonFeatures, {
        featureType,
        featureStatus,
        featureTitle,
      });
DESPRES Damien's avatar
DESPRES Damien committed
      // Fit the map to bound only if no initial zoom and center are defined
      if (
        (this.lat === "" || this.lng === "" || this.zoom === "") &&
        this.geojsonFeatures.length > 0
      ) {
        mapUtil.getMap().fitBounds(this.featureGroup.getBounds());
DESPRES Damien's avatar
DESPRES Damien committed
      this.form.type.choices = [
Timothee P's avatar
Timothee P committed
        //* converting Set to an Array with spread "..."
        ...new Set(
          this.geojsonFeatures.map((el) => el.properties.feature_type.title)
        ), //* use Set to eliminate duplicate values
      ];
      //this.$store.dispatch("GET_PROJECT_MESSAGES", this.$route.params.slug);
      this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug);
Timothee P's avatar
Timothee P committed
    this.zoom = this.$route.query.zoom || "";
    this.lat = this.$route.query.lat || "";
    this.lng = this.$route.query.lng || "";
    var mapDefaultViewCenter =
      this.$store.state.configuration.DEFAULT_MAP_VIEW.center;
    var mapDefaultViewZoom =
      this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom;
Timothee P's avatar
Timothee P committed
    this.map = mapUtil.createMap({
      zoom: this.zoom,
      lat: this.lat,
      lng: this.lng,
DESPRES Damien's avatar
DESPRES Damien committed
      mapDefaultViewCenter,
      mapDefaultViewZoom,
    });
Timothee P's avatar
Timothee P committed

    document.addEventListener("change-layers-order", (event) => {
DESPRES Damien's avatar
DESPRES Damien committed
      // Reverse is done because the first layer in order has to be added in the map in last.
      // Slice is done because reverse() changes the original array, so we make a copy first
      mapUtil.updateOrder(event.detail.layers.slice().reverse());
    });

    // --------- End sidebar events ----------
    if (this.$store.state.map.geojsonFeatures) {
      this.loadFeatures(this.$store.state.map.geojsonFeatures);
    } else {
      const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?output=geojson`;
      this.featureLoading = true;
      axios
        .get(url)
        .then((response) => {
          this.loadFeatures(response.data.features);
          this.featureLoading = false;
        })
        .catch((error) => {
          this.featureLoading = false;
          throw error;
        });
    }
    setTimeout(
      function () {
DESPRES Damien's avatar
DESPRES Damien committed
        mapUtil.addGeocoders(this.$store.state.configuration);
  height: calc(100vh - 300px);
  border: 1px solid grey;
  /* To not hide the filters */
  z-index: 1;
}

Timothee P's avatar
Timothee P committed
.center {
DESPRES Damien's avatar
DESPRES Damien committed
  text-align: center !important;
}

#form-filters,
.ui.centered > .row.feature-list-container {
  justify-content: flex-start;
}

.feature-list-container .ui.menu:not(.vertical) .right.item {
  padding-right: 0;
}

.map-container {
  width: 80vw;
  transform: translateX(-50%);
  margin-left: 50%;
}

@media screen and (max-width: 767px) {
  #form-filters > .field.column {
    width: 100% !important;
  }

  .map-container {
    width: 100%;
  }
}
/* datatables */
.dataTables_wrapper {
  position: relative;
  clear: both;
}
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_processing,
.dataTables_wrapper .dataTables_paginate {
  color: #333;
}
.dataTables_wrapper .dataTables_info {
  clear: both;
  float: left;
  padding-top: 0.755em;
}
.dataTables_wrapper .dataTables_paginate {
  float: right;
  text-align: right;
  padding-top: 0.25em;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
  color: #333 !important;
  border: 1px solid #979797;
  background-color: white;
  background: -webkit-gradient(
    linear,
    left top,
    left bottom,
    color-stop(0%, #fff),
    color-stop(100%, #dcdcdc)
  );
  background: -webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);
  background: -moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);
  background: -ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);
  background: -o-linear-gradient(top, #fff 0%, #dcdcdc 100%);
  background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%);
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
  box-sizing: border-box;
  display: inline-block;
  min-width: 1.5em;
  padding: 0.5em 1em;
  margin-left: 2px;
  text-align: center;
  text-decoration: none !important;
  cursor: pointer;
  color: #333 !important;
  border: 1px solid transparent;
  border-radius: 2px;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
  cursor: default;
  color: #666 !important;
  border: 1px solid transparent;
  background: transparent;
  box-shadow: none;
}
</style>