<template> <div class="fourteen wide column"> <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 secondary menu"> <a @click="showMap = true" :class="['item', { active: showMap }]" data-tab="map" data-tooltip="Carte" ><i class="map fitted icon"></i ></a> <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> {{ getFilteredFeatures().length }} signalement{{ getFilteredFeatures().length > 1 ? "s" : "" }} </h4> </div> <!-- {% if project and feature_types and permissions|lookup:'can_create_feature' %} --> <!-- v-if="project && feature_types && permissions" --> <!-- //Todo: add permissions --> <div v-if="project && feature_types" class="item right"> <div @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> <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> </div> </div> </div> <form id="form-filters" class="ui form grid" action="" method="get"> <div class="field wide four column"> <label>Type</label> <Dropdown @update:selection="onFilterTypeChange($event)" :options="form.type.choices" :selected="form.type.selected" :selection.sync="form.type.selected" :search="true" /> </div> <div class="field wide four column"> <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" /> </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> <div v-show="!showMap" data-tab="list" class="dataTables_wrapper no-footer"> <table id="table-features" class="ui compact table"> <thead> <tr> <th class="center">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')"/></th> </tr> </thead> <tbody> <tr v-for="(feature, index) in getPaginatedFeatures()" :key="index" > <td class="center"> <div v-if="feature.properties.status.value == 'archived'" data-tooltip="Archivé"> <i class="grey archive icon"></i> </div> <div v-else-if="feature.properties.status.value == 'pending'" data-tooltip="En attente de publication" > <i class="teal hourglass outline icon"></i> </div> <div v-else-if="feature.properties.status.value == 'published'" data-tooltip="Publié" > <i class="olive check icon"></i> </div> <div v-else-if="feature.properties.status.value == 'draft'" data-tooltip="Brouillon" > <i class="orange pencil alternate icon"></i> </div> </td> <td class="center"> <router-link :to="{ name: 'details-type-signalement', params: { feature_type_slug: feature.properties.feature_type.title }, }" > {{ feature.properties.feature_type.title }} </router-link> </td> <td class="center"> <router-link :to="{ name: 'details-signalement', params: { slug_type_signal: feature.properties.feature_type.slug, slug_signal: feature.properties.slug || feature.id, }, }" >{{ getFeatureDisplayName(feature)}}</router-link > </td> <td class="center"> <!-- |date:'Ymd' --> {{ feature.properties.updated_on }} </td> <td class="center" v-if="user"> {{ feature.properties.display_creator }} </td> </tr> <tr v-if="getFilteredFeatures().length === 0" class="odd"> <td colspan="5" class="dataTables_empty" valign="top"> Aucune donnée disponible </td> </tr> </tbody> </table> <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 {{ getFilteredFeatures().length }} éléments </div> <div class="dataTables_paginate paging_simple_numbers" id="table-features_paginate" > <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 > <!-- <span> <a v-for="(page, index) in getNbPages()" :key="'page' + index" class="paginate_button current" aria-controls="table-features" data-dt-idx="1" tabindex="0" >{{index}}</a > </span> <span class="ellipsis">…</span> --> <a class="paginate_button next" aria-controls="table-features" data-dt-idx="7" tabindex="0" id="table-features_next" @click="toNextPage" >Suivant</a > </div> </div> </div> </template> <script> import { mapGetters, mapState } from "vuex"; import L from "leaflet"; import { mapUtil } from "@/assets/js/map-util.js"; import SidebarLayers from "@/components/map-layers/SidebarLayers"; import Dropdown from "@/components/Dropdown.vue"; const axios = require("axios"); export default { name: "Feature_list", components: { SidebarLayers, Dropdown, }, data() { return { 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, }, pagination: { pagesize:15, start: 0, end: 14, }, sort:{ column:'', ascending:true }, 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"]), ...mapState(["user"]), ...mapState("feature", ["features"]), ...mapState("feature_type", ["feature_types"]), baseMaps(){ return this.$store.state.map.basemaps; }, }, methods: { getFeatureDisplayName(feature){ return feature.properties.title || feature.id; }, getPaginatedFeatures() { let filterdFeatures=[...this.getFilteredFeatures()]; // Ajout du tri 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; } if(aProp > bProp) { return 1; } return 0; } else{ //descending if(aProp < bProp) { return 1; } if(aProp > bProp) { return -1; } return 0; } }) } return filterdFeatures.slice( this.pagination.start, this.pagination.end ); }, isSortedAsc(column){ return this.sort.column==column && this.sort.ascending; }, isSortedDesc(column){ return this.sort.column==column && !this.sort.ascending; }, changeSort(column){ if(this.sort.column==column){ //changer order this.sort.ascending=!this.sort.ascending; }else{ this.sort.column=column; this.sort.ascending=true; } }, 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.value); this.filterType=newvalue.value; } this.onFilterChange(); }, onFilterChange(){ var features=this.getFilteredFeatures(); //console.log(this.getPaginatedFeatures()); this.featureGroup.clearLayers(); this.featureGroup = mapUtil.addFeatures(features, {}); if(features.length>0){ mapUtil.getMap().fitBounds(this.featureGroup.getBounds()) } }, getFilteredFeatures() { let results = this.geojsonFeatures; if (this.form.type.selected) { results = results.filter( (el) => el.properties.feature_type.title === this.form.type.selected ); } 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; }, getNbPages(){ return Math.round(this.getFilteredFeatures().length/this.pagination.pagesize); }, toPreviousPage() { if (this.pagination.start > 0) { this.pagination.start -= this.pagination.pagesize; this.pagination.end -= this.pagination.pagesize; } }, toNextPage() { if (this.pagination.end < this.getFilteredFeatures().length) { this.pagination.start += this.pagination.pagesize; this.pagination.end += this.pagination.pagesize; } }, loadFeatures(features){ this.geojsonFeatures = features; console.log(this.geojsonFeatures) 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()) } }, addGeocoders(){ let geocoder; // Get the settings.py variable SELECTED_GEOCODER_PROVIDER. This way avoids XCC attacks const geocoderLabel = this.$store.state.configuration.SELECTED_GEOCODER.PROVIDER; if (geocoderLabel) { const LIMIT_RESULTS = 5; if ( geocoderLabel === this.$store.state.configuration.GEOCODER_PROVIDERS.ADDOK ) { geocoder = L.Control.Geocoder.addok({ limit: LIMIT_RESULTS }); } else if ( geocoderLabel === this.$store.state.configuration.GEOCODER_PROVIDERS.PHOTON ) { geocoder = L.Control.Geocoder.photon(); } else if ( geocoderLabel === this.$store.state.configuration.GEOCODER_PROVIDERS.NOMINATIM ) { geocoder = L.Control.Geocoder.nominatim(); } L.Control.geocoder({ placeholder: "Chercher une adresse...", geocoder: geocoder, }).addTo(this.map); } }, }, 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.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, }); let self=this; 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 ---------- console.log(this.$store.state.map.geojsonFeatures); 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`; axios.get(url) .then((response) => { this.loadFeatures(response.data.features); }) .catch((error) => { throw error; }); } // Update zoom and center on each move mapUtil.addMapEventListener("moveend", () => { self.zoom=mapUtil.getMap().getZoom(); self.lat=mapUtil.getMap().getCenter().lat; self.lng=mapUtil.getMap().getCenter().lng; //$formFilters.find("input[name=zoom]").val(mapUtil.getMap().getZoom()) //$formFilters.find("input[name=lat]").val(mapUtil.getMap().getCenter().lat) //$formFilters.find("input[name=lng]").val(mapUtil.getMap().getCenter().lng) }); setTimeout(function () { this.addGeocoders(); }.bind(this), 1000) this.form.type.choices = [ //* converting Set to an Array with spread "..." ...new Set(this.features.map((el) => el.feature_type.title)), //* use Set to eliminate duplicate values ]; }, // todo : add script }; </script> <style> #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; } #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>