<template> <div v-frag> <div v-frag v-if="permissions && permissions.can_view_project && project"> <div id="message" class="fullwidth"> <div v-if="tempMessage" class="ui positive message"> <!-- <i class="close icon"></i> --> <!-- <div class="header">You are eligible for a reward</div> --> <p><i class="check icon"></i> {{ tempMessage }}</p> </div> </div> <div id="message_info" class="fullwidth"> <div v-if="infoMessage" class="ui info message" style="text-align: left" > <div class="header"> <i class="info circle icon"></i> Informations </div> <ul class="list"> {{ infoMessage }} </ul> </div> </div> <div class="row"> <div class="four wide middle aligned column"> <img class="ui small spaced image" :src=" project.thumbnail.includes('default') ? require('@/assets/img/default.png') : DJANGO_BASE_URL + project.thumbnail + refreshId() " /> <div class="ui hidden divider"></div> <div class="ui basic teal label" data-tooltip="Membres"> <i class="user icon"></i>{{ project.nb_contributors }} </div> <div class="ui basic teal label" data-tooltip="Signalements"> <i class="map marker icon"></i>{{ project.nb_published_features }} </div> <div class="ui basic teal label" data-tooltip="Commentaires"> <i class="comment icon"></i >{{ project.nb_published_features_comments }} </div> </div> <div class="ten wide column"> <h1 class="ui header"> <div class="content"> {{ project.title }} <div v-if="arraysOffline.length > 0"> {{ arraysOffline.length }} modifications en attente <button :disabled="isOffline()" @click="sendOfflineFeatures()" class="ui fluid teal icon button" > <i class="upload icon"></i> Envoyer au serveur </button> </div> <div class="ui icon right floated compact buttons"> <a v-if=" user && permissions && permissions.can_view_project && isOffline() !== true " id="subscribe-button" class="ui button button-hover-green" data-tooltip="S'abonner au projet" data-position="top center" data-variation="mini" @click="isModalOpen = true" > <i class="inverted grey envelope icon"></i> </a> <router-link v-if=" permissions && permissions.can_update_project && isOffline() !== true " :to="{ name: 'project_edit', params: { slug: project.slug } }" class="ui button button-hover-orange" data-tooltip="Modifier le projet" data-position="top center" data-variation="mini" > <i class="inverted grey pencil alternate icon"></i> </router-link> </div> <div class="ui hidden divider"></div> <div class="sub header"> {{ project.description }} </div> </div> </h1> </div> </div> <div class="row"> <div class="seven wide column"> <h3 class="ui header">Types de signalements</h3> <div class="ui middle aligned divided list"> <div :class="{ active: featureTypeLoading }" class="ui inverted dimmer" > <div class="ui text loader"> Récupération des types de signalements en cours... </div> </div> <div v-for="(type, index) in feature_types" :key="type.title + '-' + index" class="item" > <div class="middle aligned content"> <router-link :to="{ name: 'details-type-signalement', params: { feature_type_slug: type.slug }, }" > <img v-if="type.geom_type === 'point'" class="list-image-type" src="@/assets/img/marker.png" /> <img v-if="type.geom_type === 'linestring'" class="list-image-type" src="@/assets/img/line.png" /> <img v-if="type.geom_type === 'polygon'" class="list-image-type" src="@/assets/img/polygon.png" /> {{ type.title }} </router-link> <!-- {% if project and feature_types and permissions|lookup:'can_create_feature' %} --> <!-- // ? should we get type.is_editable ? --> <!-- v-if=" project && permissions.can_create_feature && type.is_editable " --> <router-link v-if=" project && permissions && permissions.can_create_feature " :to="{ name: 'ajouter-signalement', params: { slug_type_signal: type.slug }, }" class=" ui compact small icon right floated button button-hover-green " data-tooltip="Ajouter un signalement" data-position="left center" data-variation="mini" > <i class="ui plus icon"></i> </router-link> <router-link :to="{ name: 'dupliquer-type-signalement', params: { slug_type_signal: type.slug }, }" v-if=" project && permissions && permissions.can_create_feature_type && isOffline() !== true " class=" ui compact small icon right floated button button-hover-green " data-tooltip="Dupliquer un type de signalement" data-position="left center" data-variation="mini" > <i class="inverted grey copy alternate icon"></i> </router-link> <router-link :to="{ name: 'editer-type-signalement', params: { slug_type_signal: type.slug }, }" v-if=" project && type.is_editable && permissions && permissions.can_create_feature_type && isOffline() !== true " class=" ui compact small icon right floated button button-hover-green " data-tooltip="Éditer le type de signalement" data-position="left center" data-variation="mini" > <i class="inverted grey pencil alternate icon"></i> </router-link> <router-link :to="{ name: 'editer-symbologie-signalement', params: { slug_type_signal: type.slug }, }" v-if=" project && type.is_editable && type.geom_type === 'point' && permissions && permissions.can_create_feature_type && isOffline() != true " class=" ui compact small icon right floated button button-hover-green " data-tooltip="Éditer la symbologie du type de signalement" data-position="left center" data-variation="mini" > <i class="inverted grey paint brush alternate icon"></i> </router-link> </div> </div> <div v-if="feature_types.length === 0"> <i> Le projet ne contient pas encore de type de signalements. </i> </div> </div> <div class="nouveau-type-signalement"> <router-link v-if=" permissions && permissions.can_update_project && isOffline() !== true " :to="{ name: 'ajouter-type-signalement', params: { slug: project.slug }, }" class="ui compact basic button button-hover-green" > <i class="ui plus icon"></i>Créer un nouveau type de signalement </router-link> </div> <div class="nouveau-type-signalement"> <a v-if=" permissions && permissions.can_update_project && isOffline() !== true " class=" ui compact basic button button-hover-green important-flex align-center text-left " > <i class="ui plus icon"></i> <label class="ui" for="json_file"> <span class="label" >Créer un nouveau type de signalement à partir d'un GeoJSON</span > </label> <input type="file" accept="application/json, .json, .geojson" style="display: none" name="json_file" id="json_file" @change="onFileChange" /> </a> <br /> <div id="button-import" v-if="fileToImport.size > 0"> <button :disabled="fileToImport.size === 0" @click="toNewFeatureType" class="ui fluid teal icon button" > <i class="upload icon"></i> Lancer l'import avec le fichier {{ fileToImport.name }} </button> </div> </div> </div> <div class="seven wide column"> <div id="map" ref="map"></div> </div> </div> <div class="row"> <div class="fourteen wide column"> <div class="ui two stackable cards"> <div class="red card"> <div class="content"> <div class="center aligned header">Derniers signalements</div> <div class="center aligned description"> <div :class="{ active: featuresLoading }" class="ui inverted dimmer" > <div class="ui text loader"> Récupération des signalements en cours... </div> </div> <div class="ui relaxed list"> <div v-for="(item, index) in last_features" :key="item.title + index" class="item" > <div class="content"> <div> <router-link :to="{ name: 'details-signalement', params: { slug: project.slug, slug_type_signal: item.feature_type.slug, slug_signal: item.feature_id, }, }" >{{ item.title || item.feature_id }}</router-link > </div> <div class="description"> <i >[{{ item.created_on | setDate }}<span v-if="user && item.display_creator" >, par {{ item.display_creator }} </span> ]</i > </div> </div> </div> <i v-if="last_features.length === 0" >Aucun signalement pour le moment.</i > </div> </div> </div> </div> <div class="orange card"> <div class="content"> <div class="center aligned header">Derniers commentaires</div> <div class="center aligned description"> <div class="ui relaxed list"> <div v-for="(item, index) in last_comments" :key="'comment ' + index" class="item" > <div class="content"> <div> <router-link :to="item.related_feature.feature_url" >"{{ item.comment }}"</router-link > </div> <div class="description"> <i >[ {{ item.created_on }}<span v-if="user && item.display_author" >, par {{ item.display_author }} </span> ]</i > </div> </div> </div> <i v-if="!last_comments || last_comments.length === 0" >Aucun commentaire pour le moment.</i > </div> </div> </div> </div> </div> </div> </div> <div class="row"> <div class="fourteen wide column"> <div class="ui grey segment"> <h3 class="ui header">Paramètres du projet</h3> <div class="ui five stackable cards"> <div class="card"> <div class="center aligned content"> <h4 class="ui center aligned icon header"> <i class="disabled grey archive icon"></i> <div class="content">Délai avant archivage automatique</div> </h4> </div> <div class="center aligned extra content"> {{ project.archive_feature }} jours </div> </div> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> <i class="disabled grey trash alternate icon"></i> <div class="content"> Délai avant suppression automatique </div> </h4> </div> <div class="center aligned extra content"> {{ project.delete_feature }} jours </div> </div> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> <i class="disabled grey eye icon"></i> <div class="content"> Visibilité des signalements publiés </div> </h4> </div> <div class="center aligned extra content"> {{ project.access_level_pub_feature }} </div> </div> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> <i class="disabled grey eye icon"></i> <div class="content"> Visibilité des signalements archivés </div> </h4> </div> <div class="center aligned extra content"> {{ project.access_level_arch_feature }} </div> </div> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> <i class="disabled grey cogs icon"></i> <div class="content">Modération</div> </h4> </div> <div class="center aligned extra content"> {{ project.moderation ? "Oui" : "Non" }} </div> </div> </div> </div> </div> </div> </div> <span v-else> <i class="icon exclamation triangle"></i> <span >Vous ne disposez pas des droits nécessaires pour consulter ce projet.</span > </span> <div v-if="isModalOpen" class="ui dimmer modals page transition visible active" style="display: flex !important" > <div :class="[ 'ui mini modal subscription', { 'transition visible active': isModalOpen }, ]" > <i @click="isModalOpen = false" class="close icon"></i> <div class="ui icon header"> <i class="envelope icon"></i> Notifications du projet </div> <div class="content"> <button @click="subscribeProject" :class="['ui compact fluid button', is_suscriber ? 'red' : 'green']" > {{ is_suscriber ? "Se désabonner de ce projet" : "S'abonner à ce projet" }} </button> </div> </div> </div> </div> </template> <script> import frag from "vue-frag"; import { mapUtil } from "@/assets/js/map-util.js"; import { mapGetters, mapState } from "vuex"; import projectAPI from "@/services/project-api"; 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: "Project_details", props: ["message"], directives: { frag, }, filters: { setDate(value) { const date = new Date(value); const d = date.toLocaleDateString("fr", { year: "2-digit", month: "numeric", day: "numeric", }); return d; }, }, data() { return { infoMessage: "", arraysOffline: [], geojsonImport: [], fileToImport: { name: "", size: 0 }, slug: this.$route.params.slug, isModalOpen: false, is_suscriber: false, tempMessage: null, featureTypeLoading: true, featuresLoading: true }; }, computed: { ...mapGetters(["project", "permissions"]), ...mapState("feature_type", ["feature_types"]), ...mapState("feature", ["features"]), ...mapState(["last_comments", "user"]), DJANGO_BASE_URL() { return this.$store.state.configuration.VUE_APP_DJANGO_BASE; }, API_BASE_URL() { return this.$store.state.configuration.VUE_APP_DJANGO_API_BASE; }, last_features() { // * limit to last five element of array (looks sorted chronologically, but not sure...) return this.features.slice(-5); }, }, methods: { refreshId() { return "?ver=" + Math.random(); }, isOffline() { return navigator.onLine === false; }, checkForOfflineFeature() { let arraysOffline = []; let localStorageArray = localStorage.getItem("geocontrib_offline"); if (localStorageArray) { arraysOffline = JSON.parse(localStorageArray); this.arraysOffline = arraysOffline.filter( (x) => x.project === this.project.slug ); } }, sendOfflineFeatures() { var promises = []; this.arraysOffline.forEach((feature, index, object) => { console.log(feature); if (feature.type === "post") { promises.push( axios .post(`${this.API_BASE_URL}features/`, feature.geojson) .then((response) => { console.log(response); if (response.status === 201 && response.data) { object.splice(index, 1); } }) .catch((error) => { console.log(error); }) ); } else if (feature.type === "put") { promises.push( axios .put( `${this.API_BASE_URL}features/${feature.featureId}`, feature.geojson ) .then((response) => { console.log(response); if (response.status === 200 && response.data) { object.splice(index, 1); } }) .catch((error) => { console.log(error); }) ); } }); Promise.all(promises).then(() => { this.updateLocalStorage(); window.location.reload(); }); }, updateLocalStorage() { let arraysOffline = []; let localStorageArray = localStorage.getItem("geocontrib_offline"); if (localStorageArray) { arraysOffline = JSON.parse(localStorageArray); } let arraysOfflineOtherProject = arraysOffline.filter( (x) => x.project !== this.project.slug ); arraysOffline = arraysOfflineOtherProject.concat(this.arraysOffline); localStorage.setItem("geocontrib_offline", JSON.stringify(arraysOffline)); }, toNewFeatureType() { this.$router.push({ name: "ajouter-type-signalement", params: { geojson: this.geojsonImport, fileToImport: this.fileToImport, }, }); }, onFileChange(e) { var files = e.target.files || e.dataTransfer.files; if (!files.length) return; this.fileToImport = files[0]; // TODO : VALIDATION IF FILE IS JSON if (this.fileToImport.size > 0) { const fr = new FileReader(); fr.onload = (e) => { this.geojsonImport = JSON.parse(e.target.result); }; fr.readAsText(this.fileToImport); //* stock filename to import features afterward this.$store.commit( "feature_type/SET_FILE_TO_IMPORT", this.fileToImport ); } }, subscribeProject() { projectAPI .subscribeProject({ suscribe: !this.is_suscriber, projectSlug: this.$route.params.slug, }) .then((data) => { this.is_suscriber = data.is_suscriber; this.isModalOpen = false; if (this.is_suscriber) this.infoMessage = "Vous êtes maintenant abonné aux notifications de ce projet."; else this.infoMessage = "Vous ne recevrez plus les notifications de ce projet."; setTimeout(() => (this.infoMessage = ""), 3000); }); }, initMap() { if (this.project && this.permissions.can_view_project) { this.$store.dispatch("map/INITIATE_MAP", this.$refs.map); const url = `${this.API_BASE_URL}projects/${this.$route.params.slug}/feature/?output=geojson`; this.checkForOfflineFeature(); let 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.$store.state.feature_type.feature_types ); axios .get(url) .then((response) => { let features = response.data.features; this.arraysOffline.forEach( (x) => (x.geojson.properties.color = "red") ); features = response.data.features.concat( this.arraysOffline.map((x) => x.geojson) ); const featureGroup = mapUtil.addFeatures( features, {}, true, this.$store.state.feature_type.feature_types ); if (featureGroup && featureGroup.getLayers().length > 0) { mapUtil .getMap() .fitBounds(featureGroup.getBounds(), { padding: [25, 25] }); this.$store.commit("map/SET_GEOJSON_FEATURES", features); } else { this.$store.commit("map/SET_GEOJSON_FEATURES", []); } }) .catch((error) => { throw error; }); } }, }, created() { if (this.user) { projectAPI .getProjectSubscription({ projectSlug: this.$route.params.slug }) .then((data) => (this.is_suscriber = data.is_suscriber)); } this.$store.commit("feature_type/SET_FEATURE_TYPES", []); //* empty feature_types remaining from previous project }, mounted() { this.$store.dispatch('GET_PROJECT_INFO', this.slug) .then(() => { this.featureTypeLoading = false; setTimeout(this.initMap, 1000); }); this.$store.dispatch('feature/GET_PROJECT_FEATURES', { project_slug: this.slug }) .then(() => { this.featuresLoading = false; }); if (this.message) { this.tempMessage = this.message; document .getElementById("message") .scrollIntoView({ block: "end", inline: "nearest" }); setTimeout(() => (this.tempMessage = null), 5000); //* hide message after 5 seconds } }, }; </script> <style> @import "../../assets/resources/semantic-ui-2.4.2/semantic.min.css"; #map { width: 100%; height: 100%; min-height: 250px; } .list-image-type { margin-right: 5px; height: 25px; vertical-align: bottom; } /* // ! missing style in semantic.min.css, je ne comprends pas comment... */ .ui.right.floated.button { float: right; margin: 0 0 0 1em; } .nouveau-type-signalement { padding-top: 1em; } #button-import { padding-top: 0.5em; } .fullwidth { width: 100%; } .important-flex { display: flex !important; } .align-center { align-items: center !important; } .text-left { text-align: left !important; } </style>