<template> <div v-frag> <div v-if="feature" v-frag > <div class="row"> <div class="fourteen wide column"> <h1 class="ui header"> <div class="content"> {{ feature.title || feature.feature_id }} <div class="ui icon right floated compact buttons"> <router-link v-if="permissions && permissions.can_create_feature" :to="{ name: 'ajouter-signalement', params: { slug_type_signal: $route.params.slug_type_signal, }, }" class="ui button button-hover-orange" data-tooltip="Ajouter un signalement" data-position="bottom left" > <i class="plus fitted icon" /> </router-link> <router-link v-if=" (permissions && permissions.can_update_feature) || isFeatureCreator || isModerator " :to="{ name: 'editer-signalement', params: { slug_signal: $route.params.slug_signal, slug_type_signal: $route.params.slug_type_signal, }, }" class="ui button button-hover-orange" > <i class="inverted grey pencil alternate icon" /> </router-link> <a v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline" id="feature-delete" class="ui button button-hover-red" @click="isCanceling = true" > <i class="inverted grey trash alternate icon" /> </a> </div> <div class="ui hidden divider" /> <div class="sub header prewrap"> {{ feature.description }} </div> </div> </h1> </div> </div> <div class="row"> <div class="seven wide column"> <table class="ui very basic table"> <tbody> <div v-for="(field, index) in feature.feature_data" :key="'field' + index" v-frag > <tr v-if="field"> <td> <b>{{ field.label }}</b> </td> <td> <b> <i v-if="field.field_type === 'boolean'" :class="[ 'icon', field.value ? 'olive check' : 'grey times', ]" /> <span v-else> {{ field.value }} </span> </b> </td> </tr> </div> <tr> <td>Auteur</td> <td>{{ feature.display_creator }}</td> </tr> <tr> <td>Statut</td> <td> <i v-if="feature.status" :class="['icon', statusIcon]" /> {{ statusLabel }} </td> </tr> <tr> <td>Date de création</td> <td v-if="feature.created_on"> {{ feature.created_on | formatDate }} </td> </tr> <tr> <td>Date de dernière modification</td> <td v-if="feature.updated_on"> {{ feature.updated_on | formatDate }} </td> </tr> </tbody> </table> <h3>Liaison entre signalements</h3> <table class="ui very basic table"> <tbody> <tr v-for="(link, index) in linked_features" :key="link.feature_to.title + index" > <td v-if="link.feature_to.feature_type_slug"> {{ link.relation_type_display }} <a @click="pushNgo(link)">{{ link.feature_to.title }} </a> ({{ link.feature_to.display_creator }} - {{ link.feature_to.created_on }}) </td> </tr> </tbody> </table> </div> <div class="seven wide column"> <div id="map" ref="map" /> </div> </div> <div class="row"> <div class="seven wide column"> <h2 class="ui header"> Pièces jointes </h2> <div v-for="pj in attachments" :key="pj.id" class="ui divided items" > <div class="item"> <a class="ui tiny image" target="_blank" :href="pj.attachment_file" > <img :src=" pj.extension === '.pdf' ? require('@/assets/img/pdf.png') : pj.attachment_file " > </a> <div class="middle aligned content"> <a class="header" target="_blank" :href="pj.attachment_file" >{{ pj.title }}</a> <div class="description"> {{ pj.info }} </div> </div> </div> </div> <i v-if="attachments.length === 0" >Aucune pièce jointe associée au signalement.</i> </div> <div class="seven wide column"> <h2 class="ui header"> Activité et commentaires </h2> <div id="feed-event" class="ui feed" > <div v-for="(event, index) in events" :key="'event' + index" v-frag > <div v-if="event.event_type === 'create'" v-frag > <div v-if="event.object_type === 'feature'" class="event" > <div class="content"> <div class="summary"> <div class="date"> {{ event.created_on }} </div> Création du signalement <span v-if="user">par {{ event.display_user }}</span> </div> </div> </div> <div v-else-if="event.object_type === 'comment'" class="event" > <div class="content"> <div class="summary"> <div class="date"> {{ event.created_on }} </div> Commentaire <span v-if="user">par {{ event.display_user }}</span> </div> <div class="extra text"> {{ event.related_comment.comment }} <div v-if="event.related_comment.attachment" v-frag > <br><a :href=" DJANGO_BASE_URL + event.related_comment.attachment.url " target="_blank" ><i class="paperclip fitted icon" /> {{ event.related_comment.attachment.title }}</a> </div> </div> </div> </div> </div> <div v-else-if="event.event_type === 'update'" class="event" > <div class="content"> <div class="summary"> <div class="date"> {{ event.created_on }} </div> Signalement mis à jour <span v-if="user">par {{ event.display_user }}</span> </div> </div> </div> </div> </div> <div v-if="permissions && permissions.can_create_feature && isOnline" class="ui segment" > <form id="form-comment" class="ui form" > <div class="required field"> <label :for="comment_form.comment.id_for_label" >Ajouter un commentaire</label> <ul v-if="comment_form.comment.errors" class="errorlist" > <li> {{ comment_form.comment.errors }} </li> </ul> <textarea v-model="comment_form.comment.value" :name="comment_form.comment.html_name" rows="2" /> </div> <label>Pièce jointe (facultative)</label> <div class="two fields"> <div class="field"> <label class="ui icon button" for="attachment_file" > <i class="paperclip icon" /> <span class="label">{{ comment_form.attachment_file.value ? comment_form.attachment_file.value : "Sélectionner un fichier ..." }}</span> </label> <input id="attachment_file" type="file" accept="application/pdf, image/jpeg, image/png" style="display: none" name="attachment_file" @change="onFileChange" > </div> <div class="field"> <input id="title" v-model="comment_form.attachment_file.title" type="text" name="title" > {{ comment_form.attachment_file.errors }} </div> </div> <ul v-if="comment_form.attachment_file.errors" class="errorlist" > <li> {{ comment_form.attachment_file.errors }} </li> </ul> <button type="button" class="ui compact green icon button" @click="postComment" > <i class="plus icon" /> Poster le commentaire </button> </form> </div> </div> </div> <div v-if="isCanceling" class="ui dimmer modals page transition visible active" style="display: flex !important" > <div :class="[ 'ui mini modal subscription', { 'active visible': isCanceling }, ]" > <i class="close icon" @click="isCanceling = false" /> <div class="ui icon header"> <i class="trash alternate icon" /> Supprimer le signalement </div> <div class="actions"> <button type="button" class="ui red compact fluid button" @click="deleteFeature" > Confirmer la suppression </button> </div> </div> </div> </div> <div v-else v-frag > Pas de signalement correspondant trouvé </div> </div> </template> <script> import frag from 'vue-frag'; import { mapGetters, mapState, mapActions } from 'vuex'; import { mapUtil } from '@/assets/js/map-util.js'; import featureAPI from '@/services/feature-api'; import axios from '@/axios-client.js'; export default { name: 'FeatureDetail', directives: { frag, }, filters: { formatDate(value) { let date = new Date(value); date = date.toLocaleString().replace(',', ''); return date.substr(0, date.length - 3); //* quick & dirty way to remove seconds from date }, }, data() { return { attachments: [], comment_form: { attachment_file: { errors: null, title: '', file: null, }, comment: { id_for_label: 'add-comment', html_name: 'add-comment', errors: '', value: null, }, }, events: [], isCanceling: false, projectSlug: this.$route.params.slug, }; }, computed: { ...mapState([ 'user', 'USER_LEVEL_PROJECTS', 'isOnline', ]), ...mapState('projects', [ 'project' ]), ...mapGetters([ 'permissions', ]), ...mapState('feature', [ 'linked_features', 'statusChoices' ]), DJANGO_BASE_URL() { return this.$store.state.configuration.VUE_APP_DJANGO_BASE; }, feature() { const result = this.$store.state.feature.current_feature; return result; }, isFeatureCreator() { if (this.feature && this.user) { return this.feature.creator === this.user.id; } return false; }, isModerator() { return this.USER_LEVEL_PROJECTS && this.project && this.USER_LEVEL_PROJECTS[this.projectSlug] === 'Modérateur' ? true : false; }, statusIcon() { switch (this.feature.status) { case 'archived': return 'grey archive'; case 'pending': return 'teal hourglass outline'; case 'published': return 'olive check'; case 'draft': return 'orange pencil alternate'; default: return ''; } }, statusLabel() { const status = this.statusChoices.find( (el) => el.value === this.feature.status ); return status ? status.name : ''; }, }, created() { this.$store.commit( 'feature-type/SET_CURRENT_FEATURE_TYPE_SLUG', this.$route.params.slug_type_signal ); this.getFeatureEvents(); this.getFeatureAttachments(); this.getLinkedFeatures(); }, mounted() { this.$store.commit('DISPLAY_LOADER', 'Recherche du signalement'); if (!this.project) { // Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh axios.all([ this.$store .dispatch('projects/GET_PROJECT', this.$route.params.slug), this.$store .dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug), this.$store.dispatch('feature/GET_PROJECT_FEATURE', { project_slug: this.$route.params.slug, feature_id: this.$route.params.slug_signal })]) .then(() => { this.$store.commit('DISCARD_LOADER'); this.initMap(); }); } if (!this.feature || this.feature.feature_id !== this.$route.params.slug_signal) { this.$store.dispatch('feature/GET_PROJECT_FEATURE', { project_slug: this.$route.params.slug, feature_id: this.$route.params.slug_signal }) .then(() => { this.$store.commit('DISCARD_LOADER'); this.initMap(); }); } else { this.$store.commit('DISCARD_LOADER'); this.initMap(); } }, beforeDestroy() { this.$store.commit('CLEAR_MESSAGES'); }, methods: { ...mapActions('feature', [ 'GET_PROJECT_FEATURES' ]), pushNgo(link) { this.$router.push({ name: 'details-signalement', params: { slug_type_signal: link.feature_to.feature_type_slug, slug_signal: link.feature_to.feature_id, }, }); this.getFeatureEvents(); this.getFeatureAttachments(); this.getLinkedFeatures(); this.addFeatureToMap(); }, validateForm() { this.comment_form.comment.errors = ''; if (!this.comment_form.comment.value) { this.comment_form.comment.errors = 'Le commentaire ne peut pas être vide'; return false; } return true; }, postComment() { if (this.validateForm()) { featureAPI .postComment({ featureId: this.$route.params.slug_signal, comment: this.comment_form.comment.value, }) .then((response) => { if (response && this.comment_form.attachment_file.file) { featureAPI .postCommentAttachment({ featureId: this.$route.params.slug_signal, file: this.comment_form.attachment_file.file, fileName: this.comment_form.attachment_file.fileName, title: this.comment_form.attachment_file.title, commentId: response.data.id, }) .then(() => { this.confirmComment(); }); } else { this.confirmComment(); } }); } }, confirmComment() { this.$store.commit('DISPLAY_MESSAGE', { comment: 'Ajout du commentaire confirmé', level: 'positive' }); this.getFeatureEvents(); //* display new comment on the page this.comment_form.attachment_file.file = null; this.comment_form.attachment_file.fileName = ''; this.comment_form.attachment_file.title = ''; this.comment_form.comment.value = null; }, validateImgFile(files, handleFile) { let url = window.URL || window.webkitURL; let image = new Image(); image.onload = function () { handleFile(true); URL.revokeObjectURL(image.src); }; image.onerror = function () { handleFile(false); URL.revokeObjectURL(image.src); }; image.src = url.createObjectURL(files); }, onFileChange(e) { // * read image file const files = e.target.files || e.dataTransfer.files; const handleFile = (isValid) => { if (isValid) { this.comment_form.attachment_file.file = files[0]; //* store the file to post afterwards let title = files[0].name; this.comment_form.attachment_file.fileName = title; //* name of the file const fileExtension = title.substring(title.lastIndexOf('.') + 1); if ((title.length - fileExtension.length) > 11) { title = title.slice(0, 10) + '[...].' + fileExtension; } this.comment_form.attachment_file.title = title; //* title for display this.comment_form.attachment_file.errors = null; } else { this.comment_form.attachment_file.errors = "Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu."; } }; if (files.length) { //* exception for pdf if (files[0].type === 'application/pdf') { handleFile(true); } else { this.comment_form.attachment_file.errors = null; //* check if file is an image and pass callback to handle file this.validateImgFile(files[0], handleFile); } } }, goBackToProject(message) { this.$router.push({ name: 'project_detail', params: { slug: this.$route.params.slug, message, }, }); }, deleteFeature() { this.$store .dispatch('feature/DELETE_FEATURE', { feature_id: this.feature.feature_id }) .then((response) => { if (response.status === 204) { this.GET_PROJECT_FEATURES({ project_slug: this.$route.params.slug }); this.goBackToProject(); } }); }, initMap() { 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, { mapDefaultViewCenter, mapDefaultViewZoom, }); // Update link to feature list with map zoom and center mapUtil.addMapEventListener('moveend', function () { // update link to feature list with map zoom and center /*var $featureListLink = $("#feature-list-link") var baseUrl = $featureListLink.attr("href").split("?")[0] $featureListLink.attr("href", baseUrl +`?zoom=${this.map.getZoom()}&lat=${this.map.getCenter().lat}&lng=${this.map.getCenter().lng}`)*/ }); // Load the layers. // - if one basemap exists, we load the layers of the first one // - if not, load the default map and service options let layersToLoad = null; var baseMaps = this.$store.state.map.basemaps; var layers = this.$store.state.map.availableLayers; if (baseMaps && baseMaps.length > 0) { const basemapIndex = 0; layersToLoad = baseMaps[basemapIndex].layers; layersToLoad.forEach((layerToLoad) => { layers.forEach((layer) => { if (layer.id === layerToLoad.id) { layerToLoad = Object.assign(layerToLoad, layer); } }); }); layersToLoad.reverse(); } mapUtil.addLayers( layersToLoad, this.$store.state.configuration.DEFAULT_BASE_MAP_SERVICE, this.$store.state.configuration.DEFAULT_BASE_MAP_OPTIONS, this.$store.state.configuration.DEFAULT_BASE_MAP_SCHEMA_TYPE, ); mapUtil.getMap().dragging.disable(); mapUtil.getMap().doubleClickZoom.disable(); mapUtil.getMap().scrollWheelZoom.disable(); this.addFeatureToMap(); }, addFeatureToMap() { const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/` + `?feature_id=${this.$route.params.slug_signal}&output=geojson`; axios .get(url) .then((response) => { if (response.data.features.length > 0) { const featureGroup = mapUtil.addFeatures( response.data.features, {}, true, this.$store.state.feature_type.feature_types ); mapUtil .getMap() .fitBounds(featureGroup.getBounds(), { padding: [25, 25] }); } }) .catch((error) => { throw error; }); }, getFeatureEvents() { featureAPI .getFeatureEvents(this.$route.params.slug_signal) .then((data) => (this.events = data)); }, getFeatureAttachments() { featureAPI .getFeatureAttachments(this.$route.params.slug_signal) .then((data) => (this.attachments = data)); }, getLinkedFeatures() { featureAPI .getFeatureLinks(this.$route.params.slug_signal) .then((data) => this.$store.commit('feature/SET_LINKED_FEATURES', data) ); }, }, }; </script> <style scoped> #map { width: 100%; height: 100%; min-height: 250px; max-height: 70vh; } #feed-event .event { margin-bottom: 1em; } #feed-event .event .date { margin-right: 1em !important; } #feed-event .event .extra.text { margin-left: 107px; margin-top: 0; } .prewrap { white-space: pre-wrap; } </style>