<template> <div class="fourteen wide column"> <script type="application/javascript" :src=" baseUrl + '/resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js' " ></script> <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="showMap = false" :class="['item no-margin', { 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" : "" }} </h4> </div> <div v-if="project && feature_types && 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" 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" @click="modalAllDelete"></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 @update:selection="onFilterTypeChange($event)" :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 @update:selection="onFilterStatusChange($event)" :options="form.status.choices" :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="onFilterChange()" /> <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 // todo : brancher sur la carte probablement --> <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"></div> <SidebarLayers v-if="baseMaps && map" /> </div> <FeatureListTable v-show="!showMap" :filteredFeatures="filteredFeatures" :user="user" :checkedFeatures.sync="checkedFeatures" /> <!-- 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" > 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"; const axios = require("axios"); export default { name: "Feature_list", components: { SidebarLayers, Dropdown, FeatureListTable, }, data() { return { modalAllDeleteOpen: false, checkedFeatures: [], 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: [], filterStatus: null, filterType: null, baseUrl: this.$store.state.configuration.BASE_URL, map: null, zoom: null, lat: null, lng: null, showMap: true, showAddFeature: false, }; }, computed: { ...mapGetters(["project", "permissions"]), ...mapState(["user"]), ...mapState("feature", ["features"]), ...mapState("feature_type", ["feature_types"]), baseMaps() { return this.$store.state.map.basemaps; }, 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()); }); } return results; }, }, 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(); }, onFilterStatusChange(newvalue) { this.filterStatus = null; if (newvalue) { console.log("filter change", newvalue.value); this.filterStatus = newvalue.value; } this.onFilterChange(); }, onFilterTypeChange(newvalue) { this.filterType = null; if (newvalue) { console.log("filter change", newvalue); this.filterType = newvalue; } this.onFilterChange(); }, onFilterChange() { var features = this.filteredFeatures; this.featureGroup.clearLayers(); this.featureGroup = mapUtil.addFeatures(features, {}); if (features.length > 0) { mapUtil .getMap() .fitBounds(this.featureGroup.getBounds(), { padding: [25, 25] }); } }, 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({ 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 ---------- 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.$store.commit( "DISPLAY_LOADER", "Récupération des signalements en cours..." ); axios .get(url) .then((response) => { this.loadFeatures(response.data.features); this.$store.commit("DISCARD_LOADER"); }) .catch((error) => { this.$store.commit("DISCARD_LOADER"); throw error; }); } setTimeout( function () { mapUtil.addGeocoders(this.$store.state.configuration); }.bind(this), 1000 ); }, loadFeatures(features) { 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, }); // 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 ]; }, }, created() { if (!this.project) { //this.$store.dispatch("GET_PROJECT_MESSAGES", this.$route.params.slug); this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug); } }, mounted() { 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>