Skip to content
Snippets Groups Projects
FeatureDetail.vue 23.5 KiB
Newer Older
    <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" />
                  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" />
                  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" />
              <div class="ui hidden divider" />
      <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>
                  <i
                    v-if="feature.status"
                    :class="['icon', statusIcon]"
                  />
                </td>
              </tr>
              <tr>
                <td>Date de création</td>
                <td v-if="feature.created_on">
Timothee P's avatar
Timothee P committed
                  {{ feature.created_on | formatDate }}
                </td>
              </tr>
              <tr>
                <td>Date de dernière modification</td>
                <td v-if="feature.updated_on">
Timothee P's avatar
Timothee P committed
                  {{ feature.updated_on | formatDate }}
              </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 }})
          <div
            id="map"
            ref="map"
          />
      <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"
          >
                :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"
                >{{
                <div class="description">
                  {{ pj.info }}
                </div>
          <i
            v-if="attachments.length === 0"
          >Aucune pièce jointe associée au signalement.</i>
          <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
                              event.related_comment.attachment.url
                        ><i class="paperclip fitted icon" />
                          {{ event.related_comment.attachment.title }}</a>
              <div
                v-else-if="event.event_type === 'update'"
                class="event"
              >
                <div class="content">
                  <div class="summary">
                    <div class="date">
                      {{ event.created_on }}
                    </div>
                    <span v-if="user">par {{ event.display_user }}</span>
                  </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"
              <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"
                    id="title"
Timothee P's avatar
Timothee P committed
                    v-model="comment_form.attachment_file.title"
Timothee P's avatar
Timothee P committed
                    name="title"
Timothee P's avatar
Timothee P committed
                  {{ comment_form.attachment_file.errors }}
              <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
        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"
          />
            <i class="trash alternate icon" />
            Supprimer le signalement
          </div>
          <div class="actions">
Timothee P's avatar
Timothee P committed
            <button
              type="button"
              class="ui red compact fluid button"
              @click="deleteFeature"
Timothee P's avatar
Timothee P committed
              Confirmer la suppression
            </button>
    <div
      v-else
      v-frag
    >
      Pas de signalement correspondant trouvé
    </div>
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';
  name: 'FeatureDetail',
Timothee P's avatar
Timothee P committed
  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
    },
  },

      attachments: [],
          title: '',
          file: null,
          id_for_label: 'add-comment',
          html_name: 'add-comment',
          errors: '',
      events: [],
      isCanceling: false,
      projectSlug: this.$route.params.slug,
    ...mapState([
      'user',
Timothee P's avatar
Timothee P committed
    ...mapState('projects', [
      'project'
    ]),
    ...mapGetters([
      'permissions',
    ]),
    ...mapState('feature', [
      'linked_features',
      'statusChoices'
    ]),
      return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
    },

DESPRES Damien's avatar
DESPRES Damien committed
      const result = this.$store.state.feature.current_feature;

    isFeatureCreator() {
      if (this.feature && this.user) {
        return this.feature.creator === this.user.id;
      }
      return false;
    },
DESPRES Damien's avatar
DESPRES Damien committed
      return this.USER_LEVEL_PROJECTS && this.project &&
        this.USER_LEVEL_PROJECTS[this.projectSlug] === 'Modérateur'
    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 : '';
Timothee P's avatar
Timothee P committed

  created() {
    this.$store.commit(
Florent Lavelle's avatar
Florent Lavelle committed
      'feature-type/SET_CURRENT_FEATURE_TYPE_SLUG',
Timothee P's avatar
Timothee P committed
      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
Timothee P's avatar
Timothee P committed
          .dispatch('projects/GET_PROJECT', this.$route.params.slug),
        this.$store
          .dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug),
Timothee P's avatar
Timothee P committed
        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) {
Timothee P's avatar
Timothee P committed
      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');
Timothee P's avatar
Timothee P committed
  },

    ...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;
    },

      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();
            }
          });
Timothee P's avatar
Timothee P committed
    confirmComment() {
Timothee P's avatar
Timothee P committed
      this.$store.commit('DISPLAY_MESSAGE', { comment: 'Ajout du commentaire confirmé', level: 'positive' });
Timothee P's avatar
Timothee P committed
      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 = '';
Timothee P's avatar
Timothee P committed
      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;

Timothee P's avatar
Timothee P committed
      const handleFile = (isValid) => {
        if (isValid) {
Timothee P's avatar
Timothee P committed
          this.comment_form.attachment_file.file = files[0]; //* store the file to post afterwards
          let title = files[0].name;
Timothee P's avatar
Timothee P committed
          this.comment_form.attachment_file.fileName = title; //* name of the file
          const fileExtension = title.substring(title.lastIndexOf('.') + 1);
Timothee P's avatar
Timothee P committed
          if ((title.length - fileExtension.length) > 11) {
            title = title.slice(0, 10) + '[...].' + fileExtension;
Timothee P's avatar
Timothee P committed
          }
          this.comment_form.attachment_file.title = title; //* title for display
          this.comment_form.attachment_file.errors = null;
        } else {
Timothee P's avatar
Timothee P committed
          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);
        }
      }
leandro's avatar
leandro committed
    goBackToProject(message) {
      this.$router.push({
        name: 'project_detail',
leandro's avatar
leandro committed
        params: {
leandro's avatar
leandro committed
          message,
        },
      });
    },
        .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
            });
    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;
DESPRES Damien's avatar
DESPRES Damien committed
        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() {
leandro's avatar
leandro committed
      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`;
          if (response.data.features.length > 0) {
            const featureGroup = mapUtil.addFeatures(
              {},
              true,
              this.$store.state.feature_type.feature_types
            );
            mapUtil
              .getMap()
              .fitBounds(featureGroup.getBounds(), { padding: [25, 25] });
    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)
Timothee P's avatar
Timothee P committed
<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;
}