Skip to content
Snippets Groups Projects
Feature_detail.vue 23.2 KiB
Newer Older
      <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"></i>
                </router-link>
                <router-link
                  v-if="
                    (permissions && permissions.can_update_feature) ||
                  :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"></i>
                </router-link>
                <a
                  @click="isCanceling = true"
                  id="feature-delete"
                  class="ui button button-hover-red"
                >
                  <i class="inverted grey trash alternate icon"></i>
                </a>
              </div>
              <div class="ui hidden divider"></div>
      <div class="row">
        <div class="seven wide column">
          <table class="ui very basic table">
            <tbody>
              <div
                v-frag
                v-for="(field, index) in feature.feature_data"
                :key="'field' + index"
              >
                <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',
                        ]"
                      ></i>
                      <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]"></i>
                  {{ statusLabel }}
                </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>
                <td>Date d'archivage automatique</td>
                <td v-if="feature.archived_on">
                  {{ feature.archived_on }}
                </td>
              </tr>
              <tr>
                <td>Date de suppression automatique</td>
                <td v-if="feature.deletion_on">
                  {{ feature.deletion_on }}
                </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 }})
          <div id="map" ref="map"></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">
                :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>
          <i v-if="attachments.length === 0"
            >Aucune pièce jointe associée au signalement.</i
          >
        <div class="seven wide column">
          <h2 class="ui header">Activité et commentaires</h2>
          <div id="feed-event" class="ui feed">
            <div v-frag v-for="(event, index) in events" :key="'event' + index">
              <div v-frag v-if="event.event_type === 'create'">
                <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-frag v-if="event.related_comment.attachment">
                        <br /><a
                          :href="
                            DJANGO_BASE_URL +
                            event.related_comment.attachment.url
                          "
                          ><i class="paperclip fitted icon"></i>
                          {{ event.related_comment.attachment.title }}</a
                        >
                      </div>
              <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 && isOffline() !== true"
            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"
                ></textarea>
              <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"></i>
                    <span class="label">{{
                      comment_form.attachment_file.value
                        ? comment_form.attachment_file.value
                        : "Sélectionner un fichier ..."
                    }}</span>
                  </label>
                  <input
                    type="file"
                    accept="application/pdf, image/jpeg, image/png"
                    style="display: none"
                    name="attachment_file"
                    id="attachment_file"
                    @change="onFileChange"
Timothee P's avatar
Timothee P committed
                    v-model="comment_form.attachment_file.title"
Timothee P's avatar
Timothee P committed
                    name="title"
                    id="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
                @click="postComment"
                type="button"
                class="ui compact green icon button"
              >
                <i class="plus icon"></i> Poster le commentaire
              </button>
            </form>
          </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 @click="isCanceling = false" class="close icon"></i>
          <div class="ui icon header">
            <i class="trash alternate icon"></i>
            Supprimer le signalement
          </div>
          <div class="actions">
            <form
              action="{% url 'geocontrib:feature_delete' slug=feature.project.slug feature_type_slug=feature.feature_type.slug feature_id=feature.feature_id %}"
              method="POST"
              <input type="hidden" name="_method" value="delete" />
              <button
                @click="deleteFeature"
                type="button"
                class="ui red compact fluid button"
              >
                Confirmer la suppression
              </button>
            </form>
          </div>
    <div v-frag v-else>Pas de signalement correspondant trouvé</div>
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: "Feature_detail",

  directives: {
    frag,
  },

  data() {
    return {
      isCanceling: false,
      attachments: [],
      events: [],
      comment_form: {
        attachment_file: {
          errors: null,
Timothee P's avatar
Timothee P committed
          title: "",
          file: null,
        },
        comment: {
          id_for_label: "add-comment",
          html_name: "add-comment",
          errors: "",
    ...mapState([
      'user',
      'USER_LEVEL_PROJECTS'
    ]),
    ...mapGetters([
      'permissions',
      'project'
    ]),
    ...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.project.slug] === "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 : "";
    },
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
    },
  },

    ...mapActions('feature', [
      'GET_PROJECT_FEATURES'
    ]),
    isOffline() {
      return navigator.onLine == false;
    },
    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) {
Timothee P's avatar
Timothee P committed
        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,
Timothee P's avatar
Timothee P committed
                fileName: this.comment_form.attachment_file.fileName,
Timothee P's avatar
Timothee P committed
                title: this.comment_form.attachment_file.title,
Timothee P's avatar
Timothee P committed
                commentId: response.data.id,
              })
              .then(() => {
Timothee P's avatar
Timothee P committed
                this.confirmComment();
Timothee P's avatar
Timothee P committed
            this.confirmComment();
Timothee P's avatar
Timothee P committed
    confirmComment() {
      this.$store.commit("DISPLAY_MESSAGE", "Ajout du commentaire confirmé");
      this.getFeatureEvents(); //* display new comment on the page
      this.comment_form.attachment_file.file = null;
Timothee P's avatar
Timothee P committed
      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
          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 {
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",
        params: {
          slug: this.$store.state.project_slug,
          message,
        },
      });
    },
      this.$store
        .dispatch("feature/DELETE_FEATURE", 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
      );

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

  created() {
    this.$store.commit(
      "feature_type/SET_CURRENT_FEATURE_TYPE_SLUG",
      this.$route.params.slug_type_signal
    );
    this.getFeatureEvents();
    this.getFeatureAttachments();
    this.getLinkedFeatures();
    this.$store.commit("DISPLAY_LOADER", "Recherche du signalement");
DESPRES Damien's avatar
DESPRES Damien committed
      // Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh
      axios.all([
        this.$store
        .dispatch("GET_PROJECT_INFO", this.$route.params.slug),
DESPRES Damien's avatar
DESPRES Damien committed
        this.$store.dispatch('feature/GET_PROJECT_FEATURE', {
          project_slug: this.$route.params.slug,
          feature_id: this.$route.params.slug_signal
DESPRES Damien's avatar
DESPRES Damien committed
        })])
        .then(() => {
          this.$store.commit("DISCARD_LOADER");
DESPRES Damien's avatar
DESPRES Damien committed
    } 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();
        });

  beforeDestroy() {
    this.$store.commit("CLEAR_MESSAGES");
  },
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;
}