Skip to content
Snippets Groups Projects
Feature_list.vue 21.65 KiB
<template>
  <div class="fourteen wide column">
    
    <div id="feature-list-container" class="ui grid mobile-column">
      <div class="four wide column mobile-fullwidth">
        <h1>Signalements</h1>
      </div>
      <div class="twelve-wide column no-padding-mobile mobile-fullwidth">
        <div class="ui large text loader">Chargement</div>
        <div class="ui secondary menu no-margin">
          <a
            @click="showMap = true"
            :class="['item no-margin', { active: showMap }]"
            data-tab="map"
            data-tooltip="Carte"
            ><i class="map fitted icon"></i
          ></a>
          <a
            @click="showTable"
            :class="['item no-margin', { active: !showMap }]"
            data-tab="list"
            data-tooltip="Liste"
            ><i class="list fitted icon"></i
          ></a>
          <div class="item">
            <h4>
              <!-- {{ featuresCount }} signalement{{ featuresCount > 1 ? "s" : "" }} -->
              {{ filteredFeatures.length }} signalement{{
                filteredFeatures.length > 1 ? "s" : ""
              }}
            </h4>
          </div>

          <div
            v-if="
              project &&
              feature_types.length > 0 &&
              permissions.can_create_feature
            "
            class="item right"
          >
            <div
              @click="showAddFeature = !showAddFeature"
              class="ui dropdown button compact button-hover-green"
              data-tooltip="Ajouter un signalement"
              data-position="bottom left"
            >
              <i class="plus fitted icon"></i>
              <div
                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',
                      params: { slug_type_signal: type.slug },
                    }"
                    v-for="(type, index) in feature_types"
                    :key="type.slug + index"
                    class="item"
                  >
                    {{ type.title }}
                  </router-link>
                </div>
              </div>
            </div>

            <div
              v-if="checkedFeatures.length"
              @click="modalAllDelete"
              class="ui button compact button-hover-red margin-left-25"
              data-tooltip="Effacer tous les types de signalements sélectionnés"
              data-position="left center"
              data-variation="mini"
            >
              <i class="grey trash fitted icon"></i>
            </div>
          </div>
        </div>
      </div>
    </div>

    <form id="form-filters" class="ui form grid" action="" method="get">
      <div class="field wide four column no-margin-mobile">
        <label>Type</label>
        <Dropdown
          v-on:update:selection="updateTypeFeatures"
          :options="form.type.choices"
          :selected="form.type.selected"
          :selection.sync="form.type.selected"
          :search="true"
          :clearable="true"
        />
      </div>
      <div class="field wide four column no-padding-mobile no-margin-mobile">
        <label>Statut</label>
        <!--  //* giving an object mapped on key name -->
        <Dropdown
          v-on:update:selection="updateStatusFeatures"
          :options="statusChoices"
          :selected="form.status.selected.name"
          :selection.sync="form.status.selected"
          :search="true"
          :clearable="true"
        />
      </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">
            <input
              type="text"
              name="title"
              v-model="form.title"
              @input="fetchPagedFeatures"
            />
            <button
              type="button"
              class="ui teal icon button"
              id="submit-search"
            >
              <i class="search icon"></i>
            </button>
          </div>
        </div>
      </div>
      <!-- map params, updated on map move -->
      <input type="hidden" name="zoom" v-model="zoom" />
      <input type="hidden" name="lat" v-model="lat" />
      <input type="hidden" name="lng" v-model="lng" />
    </form>

    <div v-show="showMap" class="ui tab active map-container" data-tab="map">
      <div id="map" ref="map"></div>
      <SidebarLayers v-if="basemaps && map" />
    </div>
    <!-- | -->
    <FeatureListTable
      v-show="!showMap"
      v-on:update:page="handlePageChange"
      v-on:update:sort="handleSortChange"
      :geojsonFeatures="geojsonFeaturesPaginated"
      :checkedFeatures.sync="checkedFeatures"
      :featuresCount="featuresCount"
      :pagination="pagination"
      :pageNumbers="pageNumbers"
    />

    <!-- MODAL ALL DELETE FEATURE TYPE -->
    <div
      v-if="modalAllDeleteOpen"
      class="ui dimmer modals page transition visible active"
      style="display: flex !important"
    >
      <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>
          Êtes-vous sûr de vouloir effacer
          <span v-if="checkedFeatures.length === 1"> un signalement ? </span>
          <span v-else> ces {{ checkedFeatures.length }} signalements ? </span>
        </div>
        <div class="actions">
          <button
            @click="deleteAllFeatureSelection"
            type="button"
            class="ui red compact fluid button no-margin"
          >
            Confirmer la suppression
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters, mapState } from "vuex";
import { mapUtil } from "@/assets/js/map-util.js";
import SidebarLayers from "@/components/map-layers/SidebarLayers";
import FeatureListTable from "@/components/feature/FeatureListTable";
import Dropdown from "@/components/Dropdown.vue";
import axios from "@/axios-client.js";

// axios.defaults.headers.common['X-CSRFToken'] = (name => {
//   var re = new RegExp(name + "=([^;]+)");
//   var value = re.exec(document.cookie);
//   return (value !== null) ? unescape(value[1]) : null;
// })('csrftoken');

export default {
  name: "Feature_list",

  components: {
    SidebarLayers,
    Dropdown,
    FeatureListTable,
  },

  data() {
    return {
      modalAllDeleteOpen: false,
      form: {
        type: {
          selected: null,
          choices: [],
        },
        status: {
          selected: {
            name: null,
            value: null,
          },
          choices: [
            {
              name: "Brouillon",
              value: "draft",
            },
            {
              name: "En attente de publication",
              value: "pending",
            },
            {
              name: "Publié",
              value: "published",
            },
            {
              name: "Archivé",
              value: "archived",
            },
          ],
        },
        title: null,
      },

      geojsonFeatures: [],
      geojsonFeaturesPaginated: [],
      baseUrl: this.$store.state.configuration.BASE_URL,
      map: null,
      zoom: null,
      lat: null,
      lng: null,
      //limit: 15,
      //offset: 0,
      featuresCount: 0,
      filterType: null,
      filterStatus: null,
      pagination: {
        currentPage: 1,
        pagesize: 15,
        start: 0,
        end: 15,
      },
      previous: null,
      next: null,
      showMap: true,
      showAddFeature: false,
      paginatedFeaturesDone: true,
    };
  },

  computed: {
    ...mapGetters(["project", "permissions"]),
    ...mapState("feature", ["checkedFeatures"]),
    ...mapState("feature_type", ["feature_types"]),
    ...mapState("map", ["basemaps"]),

    API_BASE_URL() {
      return this.$store.state.configuration.VUE_APP_DJANGO_API_BASE;
    },

    statusChoices() {
      //* if project is not moderate, remove pending status
      return this.form.status.choices.filter((el) =>
        this.project && this.project.moderation ? true : el.value !== "pending"
      );
    },

    pageNumbers() {
      const totalPages = Math.ceil(
        this.featuresCount / this.pagination.pagesize
      );
      return [...Array(totalPages).keys()].map((pageNumb) => {
        ++pageNumb;
        return pageNumb;
      });
    },

    filteredFeatures() {
      let results = this.geojsonFeatures;
      if (this.form.type.selected) {
        results = results.filter(
          (el) => el.properties.feature_type.title === this.form.type.selected
        );
      }
      if (this.form.status.selected.value) {
        console.log("filter by" + this.form.status.selected.value);
        results = results.filter(
          (el) => el.properties.status.value === this.form.status.selected.value
        );
      }
      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());
        });
      }
      return results;
    },
  },

  watch: {
    filteredFeatures(newValue, oldValue) {
      if (newValue && newValue !== oldValue) {
        this.onFilterChange();
      }
    },
  },

  methods: {
    showTable() {
      if (this.paginatedFeaturesDone) {
        this.fetchPagedFeatures();
        this.paginatedFeaturesDone = false;
      }
      this.showMap = false;
    },

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

    deleteFeature(feature_id) {
      const url = `${this.API_BASE_URL}features/${feature_id}/?project__slug=${this.project.slug}`;
      axios
        .delete(url, {})
        .then(() => {
          if (!this.modalAllDeleteOpen) {
            this.$store
              .dispatch("feature/GET_PROJECT_FEATURES", {
                project_slug: this.project.slug,
              })
              .then(() => {
                this.fetchPagedFeatures();
                this.getNloadGeojsonFeatures();
                this.checkedFeatures.splice(feature_id);
              });
          }
        })
        .catch(() => {
          return false;
        });
    },

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

    onFilterChange() {
      if (this.featureGroup) {
        const features = this.filteredFeatures;
        this.featureGroup.clearLayers();
        this.featureGroup = mapUtil.addFeatures(
          features,
          {},
          true,
          this.feature_types
        );
        mapUtil.getMap().invalidateSize();
        mapUtil.getMap()._onResize(); // force refresh for vector tiles
        if(window.layerMVT) {
          window.layerMVT.redraw();
        }
        
        if (this.featureGroup.getLayers().length > 0) {
          mapUtil
            .getMap()
            .fitBounds(this.featureGroup.getBounds(), { padding: [25, 25] });
        } else {
          mapUtil.getMap().zoomOut(1);
        }
      }
    },

    initMap() {
      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;

      this.map = mapUtil.createMap(this.$refs.map, {
        zoom: this.zoom,
        lat: this.lat,
        lng: this.lng,
        mapDefaultViewCenter,
        mapDefaultViewZoom,
      });

      document.addEventListener("change-layers-order", (event) => {
        // 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 ----------
      //this.fetchPagedFeatures();
      this.getNloadGeojsonFeatures();

      setTimeout(() => {
        const project_id = this.$route.params.slug.split("-")[0];
        const mvtUrl = `${this.API_BASE_URL}features.mvt/?tile={z}/{x}/{y}&project_id=${project_id}`;
        mapUtil.addVectorTileLayer(
          mvtUrl,
          this.$route.params.slug,
          this.feature_types,
          this.form
        );
        mapUtil.addGeocoders(this.$store.state.configuration);
      }, 1000);
    },

    getNloadGeojsonFeatures() {
      const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?output=geojson`;
      this.$store.commit(
        "DISPLAY_LOADER",
        "Récupération des signalements en cours..."
      );
      axios
        .get(url)
        .then((response) => {
          if (response.status === 200 && response.data.features.length > 0) {
            this.geojsonFeatures = response.data.features;
            this.loadFeatures();
          }
          this.$store.commit("DISCARD_LOADER");
        })
        .catch((error) => {
          this.$store.commit("DISCARD_LOADER");
          throw error;
        });
    },

    loadFeatures() {
      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,
        },
        true,
        this.feature_types
      );
      // 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(), { padding: [25, 25] });
      }
      this.form.type.choices = [
        //* converting Set to an Array with spread "..."
        ...new Set(
          this.geojsonFeatures.map((el) => el.properties.feature_type.title)
        ), //* use Set to eliminate duplicate values
      ];
    },

    //* Paginated Features for table *//
    getFeatureTypeSlug(title) {
      const featureType = this.feature_types.find((el) => el.title === title);
      return featureType ? featureType.slug : null;
    },

    buildFilterParams({ filterType, filterValue }) {
      let params = "";
      let typeFilter, statusFilter;
      //*** feature type ***//
      if (filterType === "featureType") {
        if (filterValue === "" && !this.form.type.selected) {
          //* s'il y n'avait pas de filtre et qu'il a été supprimé --> ne pas mettre à jour les features
          return "abort";
        } else if (filterValue !== undefined && filterValue !== null) {
          //* s'il y a un nouveau filtre --> ajouter une params
          typeFilter = this.getFeatureTypeSlug(filterValue);
        } //* sinon il n'y a pas de param ajouté, ce qui supprime la query

        //*** status ***//
      } else if (filterType === "status") {
        if (filterValue === "" && !this.form.status.selected.value) {
          return "abort";
        } else if (filterValue !== undefined && filterValue !== null) {
          statusFilter = filterValue.value;
        }
      }

      //* after possibilities of aborting features fetch, empty geojson to make sure even no result would update

      if (
        (filterType === undefined || filterType === "status") &&
        this.form.type.selected
      ) {
        //* s'il y a déjà un filtre sélectionné, maintenir le params
        typeFilter = this.getFeatureTypeSlug(this.form.type.selected);
      }
      if (
        (filterType === undefined || filterType === "featureType") &&
        this.form.status.selected.value
      ) {
        statusFilter = this.form.status.selected.value;
      }

      if (typeFilter) {
        let typeParams = `&feature_type_slug=${typeFilter}`;
        params += typeParams;
      }
      if (statusFilter) {
        let statusParams = `&status__value=${statusFilter}`;
        params += statusParams;
      }

      //*** title ***//
      if (this.form.title) {
        params += `&title=${this.form.title}`;
      }

      return params;
    },

    updateTypeFeatures(filterValue) {
      //* only update:selection custom event can trigger the filter update,
      //* but it happens before the value is updated, thus using selected value from event to update query
      this.fetchPagedFeatures({ filterType: "featureType", filterValue });
    },

    updateStatusFeatures(filterValue) {
      this.fetchPagedFeatures({ filterType: "status", filterValue });
    },

    fetchPagedFeatures(params) {
      // this.onFilterChange(); //* temporary, use paginated event to watch change in filters, to modify geojson on map
      //* replace function calls in watcher and on input
      let url = `${this.API_BASE_URL}projects/${this.$route.params.slug}/feature-paginated/?output=geojson&limit=${this.pagination.pagesize}&offset=${this.pagination.start}`;

      if (params) {
        if (typeof params === "object") {
          const filterParams = this.buildFilterParams(params);
          if (filterParams === "abort") return;
          url += filterParams;
        } else {
          //console.error("ONLY FOR DEV !!!!!!!!!!!!!");
          //params = params.replace("8000", "8010");
          //console.log(url);
          url = params;
        }
      }

      this.$store.commit(
        "DISPLAY_LOADER",
        "Récupération des signalements en cours..."
      );
      axios
        .get(url)
        .then((response) => {
          if (response.status === 200) {
            this.featuresCount = response.data.count;
            this.previous = response.data.previous;
            this.next = response.data.next;
            this.geojsonFeaturesPaginated = response.data.results.features;
            //if (response.data.results.features.length > 0) {
            //this.loadFeatures();
            //this.onFilterChange();
            //}
          }
          this.$store.commit("DISCARD_LOADER");
        })
        .catch((error) => {
          this.$store.commit("DISCARD_LOADER");
          throw error;
        });
    },

    //* Pagination for table *//

    handlePageChange(page) {
      if (page === "next") {
        this.toNextPage();
      } else if (page === "previous") {
        this.toPreviousPage();
      } else if (typeof page === "number") {
        //* update limit and offset
        this.toPage(page);
      }
    },

    handleSortChange(ordering) {
      console.log("ORDERING", ordering)
    },

    toPage(pageNumber) {
      const toAddOrRemove =
        (pageNumber - this.pagination.currentPage) * this.pagination.pagesize;
      this.pagination.start += toAddOrRemove;
      this.pagination.end += toAddOrRemove;
      this.pagination.currentPage = pageNumber;
      this.fetchPagedFeatures();
    },

    toPreviousPage() {
      if (this.pagination.currentPage !== 1) {
        if (this.pagination.start > 0) {
          this.pagination.start -= this.pagination.pagesize;
          this.pagination.end -= this.pagination.pagesize;
          this.pagination.currentPage -= 1;
        }
        this.fetchPagedFeatures(this.previous);
      }
    },

    toNextPage() {
      if (this.pagination.currentPage !== this.pageNumbers.length) {
        if (this.pagination.end < this.featuresCount) {
          this.pagination.start += this.pagination.pagesize;
          this.pagination.end += this.pagination.pagesize;
          this.pagination.currentPage += 1;
        }
        this.fetchPagedFeatures(this.next);
      }
    },
  },

  mounted() {
    if (!this.project) {
      // Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh
      this.$store
        .dispatch("GET_PROJECT_INFO", this.$route.params.slug)
        .then(() => {
          this.initMap();
        });
    } else {
      this.initMap();
    }
  },

  destroyed() {
    //* allow user to change page if ever stuck on loader
    this.$store.commit("DISCARD_LOADER");
  },
};
</script>


<style scoped>
#map {
  width: 100%;
  min-height: 300px;
  height: calc(100vh - 300px);
  border: 1px solid grey;
  /* To not hide the filters */
  z-index: 1;
}

.center {
  text-align: center !important;
}

#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%;
}

.margin-left-25 {
  margin-left: 0.25em !important;
}

.no-margin {
  margin: 0 !important;
}

.no-padding {
  padding: 0 !important;
}

@media screen and (min-width: 767px) {
  .twelve-wide {
    width: 75% !important;
  }
}
@media screen and (max-width: 767px) {
  #feature-list-container > .mobile-fullwidth {
    width: 100% !important;
  }

  .no-margin-mobile {
    margin: 0 !important;
  }

  .no-padding-mobile {
    padding-top: 0 !important;
    padding-bottom: 0 !important;
  }
  .mobile-column {
    flex-direction: column !important;
  }
  #form-filters > .field.column {
    width: 100% !important;
  }

  .map-container {
    width: 100%;
  }
}
</style>