Skip to content
Snippets Groups Projects
Commit 8c856939 authored by Timothee P's avatar Timothee P :sunflower:
Browse files

Merge branch 'redmine-issues/25836' into 'develop'

REDMINE_ISSUE-25836 | Publication en masse des signalements

See merge request !861
parents 8307b68b 4fc4fe8c
No related branches found
No related tags found
1 merge request!861REDMINE_ISSUE-25836 | Publication en masse des signalements
...@@ -64,7 +64,26 @@ ...@@ -64,7 +64,26 @@
scope="col" scope="col"
class="dt-center" class="dt-center"
> >
Sélection <div
v-if="massMode === 'edit-status'"
class="ui checkbox"
>
<input
id="select-all"
v-model="isAllSelected"
type="checkbox"
name="select-all"
>
<label for="select-all">
<span v-if="!isAllSelected">
Tout sélectionner
</span>
<span v-else>
Tout désélectionner
</span>
</label>
</div>
<span v-else>Sélection</span>
</th> </th>
<th <th
...@@ -215,14 +234,14 @@ ...@@ -215,14 +234,14 @@
class="dt-center" class="dt-center"
> >
<div <div
:class="['ui checkbox', {disabled: !checkRights(feature)}]" :class="['ui checkbox', {disabled: isAllSelected || !checkRights(feature) }]"
> >
<input <input
:id="feature.feature_id" :id="feature.feature_id"
v-model="checked" v-model="checked"
type="checkbox" type="checkbox"
:value="feature.feature_id" :value="feature.feature_id"
:disabled="!checkRights(feature)" :disabled="isAllSelected || !checkRights(feature)"
name="select" name="select"
@input="storeClickedFeature(feature)" @input="storeClickedFeature(feature)"
> >
...@@ -450,6 +469,10 @@ export default { ...@@ -450,6 +469,10 @@ export default {
type: Array, type: Array,
default: null, default: null,
}, },
allSelected: {
type: Boolean,
default: false,
},
checkedFeatures: { checkedFeatures: {
type: Array, type: Array,
default: null, default: null,
...@@ -492,6 +515,8 @@ export default { ...@@ -492,6 +515,8 @@ export default {
}, },
set(newMode) { set(newMode) {
this.TOGGLE_MASS_MODE(newMode); this.TOGGLE_MASS_MODE(newMode);
// Reset all selections
this.isAllSelected = false;
this.UPDATE_CLICKED_FEATURES([]); this.UPDATE_CLICKED_FEATURES([]);
this.UPDATE_CHECKED_FEATURES([]); this.UPDATE_CHECKED_FEATURES([]);
}, },
...@@ -503,13 +528,22 @@ export default { ...@@ -503,13 +528,22 @@ export default {
checked: { checked: {
get() { get() {
return this.checkedFeatures; return this.isAllSelected || this.checkedFeatures;
}, },
set(newChecked) { set(newChecked) {
this.$store.commit('feature/UPDATE_CHECKED_FEATURES', newChecked); this.$store.commit('feature/UPDATE_CHECKED_FEATURES', newChecked);
}, },
}, },
isAllSelected: {
get() {
return this.allSelected;
},
set(isChecked) {
this.$emit('update:allSelected', isChecked);
},
},
displayedPageEnd() { displayedPageEnd() {
return this.featuresCount <= this.pagination.end return this.featuresCount <= this.pagination.end
? this.featuresCount ? this.featuresCount
...@@ -734,6 +768,13 @@ i.icon.sort:not(.down):not(.up) { ...@@ -734,6 +768,13 @@ i.icon.sort:not(.down):not(.up) {
background-color: #fbf5f5;; background-color: #fbf5f5;;
} }
#select-all + label {
text-align: left;
&:hover {
cursor: pointer;
}
}
@media only screen and (min-width: 761px) { @media only screen and (min-width: 761px) {
.table-mobile-buttons { .table-mobile-buttons {
display: none !important; display: none !important;
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
</div> </div>
</div> </div>
<div <div
v-if="checkedFeatures.length > 0 && massMode.includes('edit') && isOnline" v-if="(allSelected || checkedFeatures.length > 0) && massMode.includes('edit') && isOnline"
id="edit-button" id="edit-button"
class="ui dropdown button compact button-hover-green tiny-margin-left" class="ui dropdown button compact button-hover-green tiny-margin-left"
:data-tooltip="`Modifier le${massMode.includes('status') ? ' statut' : 's attributs'} des signalements`" :data-tooltip="`Modifier le${massMode.includes('status') ? ' statut' : 's attributs'} des signalements`"
...@@ -243,11 +243,14 @@ export default { ...@@ -243,11 +243,14 @@ export default {
}; };
} }
}, },
allSelected: {
type: Boolean,
default: false,
},
editAttributesFeatureType: { editAttributesFeatureType: {
type: String, type: String,
default: null, default: null,
}, },
}, },
data() { data() {
......
import axios from '@/axios-client.js'; import axios from '@/axios-client.js';
import store from '../store'; import store from '../store';
import { cleanParams } from '@/utils';
const featureAPI = { const featureAPI = {
...@@ -106,8 +107,6 @@ const featureAPI = { ...@@ -106,8 +107,6 @@ const featureAPI = {
return null; return null;
} }
}, },
// todo : fonction pour faire un post ou un put du signalement
async postOrPutFeature({ method, feature_id, feature_type__slug, project__slug, data }) { async postOrPutFeature({ method, feature_id, feature_type__slug, project__slug, data }) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
...@@ -132,7 +131,7 @@ const featureAPI = { ...@@ -132,7 +131,7 @@ const featureAPI = {
async updateFeature({ feature_id, feature_type__slug, project__slug, newStatus }) { async updateFeature({ feature_id, feature_type__slug, project__slug, newStatus }) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const url = `${baseUrl}features/${feature_id}/?feature_type__slug=${feature_type__slug}&project__slug=${project__slug}`; const url = `${baseUrl}v2/features/${feature_id}/?feature_type__slug=${feature_type__slug}&project__slug=${project__slug}`;
const response = await axios({ const response = await axios({
url, url,
...@@ -146,6 +145,23 @@ const featureAPI = { ...@@ -146,6 +145,23 @@ const featureAPI = {
} }
}, },
async updateAllFeatureStatus(queryparams, newStatus) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const queryString = new URLSearchParams(cleanParams(queryparams)).toString();
const url = `${baseUrl}v2/features/bulk-update-status/?${queryString}`;
const response = await axios({
url,
method: 'POST',
data: { status: newStatus }
});
if (response.status === 200 && response.data) {
return response;
} else {
return null;
}
},
async postComment({ featureId, comment }) { async postComment({ featureId, comment }) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const response = await axios.post( const response = await axios.post(
......
...@@ -289,4 +289,9 @@ export function formatUserOption(user) { ...@@ -289,4 +289,9 @@ export function formatUserOption(user) {
name: [name, user.username], name: [name, user.username],
value: user.id, value: user.id,
}; };
} }
\ No newline at end of file
export const cleanParams = (params) =>
Object.fromEntries(
Object.entries(params).filter(([_, value]) => value != null) // Exclut null et undefined
);
\ No newline at end of file
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
:show-map="showMap" :show-map="showMap"
:features-count="featuresCountDisplay" :features-count="featuresCountDisplay"
:pagination="pagination" :pagination="pagination"
:all-selected="allSelected"
:edit-attributes-feature-type="editAttributesFeatureType" :edit-attributes-feature-type="editAttributesFeatureType"
@set-filter="setFilters" @set-filter="setFilters"
@reset-pagination="resetPagination" @reset-pagination="resetPagination"
...@@ -48,6 +49,7 @@ ...@@ -48,6 +49,7 @@
v-show="!showMap" v-show="!showMap"
:paginated-features="paginatedFeatures" :paginated-features="paginatedFeatures"
:page-numbers="pageNumbers" :page-numbers="pageNumbers"
:all-selected="allSelected"
:checked-features.sync="checkedFeatures" :checked-features.sync="checkedFeatures"
:features-count="featuresCount" :features-count="featuresCount"
:pagination="pagination" :pagination="pagination"
...@@ -56,6 +58,7 @@ ...@@ -56,6 +58,7 @@
:queryparams="queryparams" :queryparams="queryparams"
@update:page="handlePageChange" @update:page="handlePageChange"
@update:sort="handleSortChange" @update:sort="handleSortChange"
@update:allSelected="handleAllSelectedChange"
/> />
<Transition name="fadeIn"> <Transition name="fadeIn">
<div <div
...@@ -143,6 +146,7 @@ export default { ...@@ -143,6 +146,7 @@ export default {
data() { data() {
return { return {
allSelected: false,
editAttributesFeatureType: null, editAttributesFeatureType: null,
currentLayer: null, currentLayer: null,
featuresCount: 0, featuresCount: 0,
...@@ -244,7 +248,7 @@ export default { ...@@ -244,7 +248,7 @@ export default {
setShowMap(newValue) { setShowMap(newValue) {
this.showMap = newValue; this.showMap = newValue;
//* expanded sidebar is visible under the list, even when the map is closed (position:absolute), solved by closing it whin switching to list // expanded sidebar is visible under the list, even when the map is closed (position:absolute), solved by closing it when switching to list
if (newValue === false && this.$refs.sidebar) this.$refs.sidebar.toggleSidebar(false); if (newValue === false && this.$refs.sidebar) this.$refs.sidebar.toggleSidebar(false);
}, },
resetPagination() { resetPagination() {
...@@ -269,60 +273,156 @@ export default { ...@@ -269,60 +273,156 @@ export default {
this.isDeleteModalOpen = !this.isDeleteModalOpen; this.isDeleteModalOpen = !this.isDeleteModalOpen;
}, },
selectAllFeatures() {
// Mettre à jour le statut allSelected
this.allSelected = true;
},
unSelectAllFeatures() {
// Vider le contenu de checkedFeatures
this.checkedFeatures = [];
},
/**
* Modifie le statut des objets sélectionnés.
*
* Cette méthode prend en charge deux cas :
* 1. Si tous les objets sont sélectionnés (`allSelected`), une requête unique en mode "bulk update" est envoyée
* au backend pour modifier le statut de tous les objets correspondant aux critères.
* 2. Si des objets spécifiques sont sélectionnés (`checkedFeatures`), ils sont traités un par un de manière
* récursive. Chaque objet modifié est retiré de la liste des objets sélectionnés.
*
* En cas d'erreur (réseau ou backend), un message d'erreur est affiché, et les données sont rafraîchies.
* Si tous les objets sont modifiés avec succès, un message de confirmation est affiché.
*
* @param {string} newStatus - Le nouveau statut à appliquer aux objets sélectionnés.
* @returns {Promise<void>} - Une promesse qui se résout lorsque tous les objets ont été traités.
*/
async modifyStatus(newStatus) { async modifyStatus(newStatus) {
if (this.checkedFeatures.length > 0) { if (this.allSelected) {
const feature_id = this.checkedFeatures[0]; // Cas : Modification en masse de tous les objets
const feature = this.clickedFeatures.find((el) => el.feature_id === feature_id); try {
const response = await featureAPI.updateAllFeatureStatus(
{
project__slug: this.projectSlug,
feature_type__slug: this.queryparams.feature_type_slug?.[0] || null,
status__value: this.queryparams.status__value?.[0] || null,
title__icontains: this.queryparams.title || null,
},
newStatus
);
if (response && response.data) {
// Affiche un message basé sur la réponse du backend
this.DISPLAY_MESSAGE({
comment: response.data.message,
level: response.data.level,
});
}
} catch (error) {
// Gère les erreurs de type Axios (400, 500, etc.)
if (error.response && error.response.data) {
this.DISPLAY_MESSAGE({
comment: error.response.data.error || 'Une erreur est survenue.',
level: 'negative',
});
} else {
// Gère les erreurs réseau ou autres
this.DISPLAY_MESSAGE({
comment: 'Impossible de communiquer avec le serveur.',
level: 'negative',
});
}
}
// Rafraîchit les données après un traitement global
this.fetchPagedFeatures();
} else if (this.checkedFeatures.length > 0) {
// Cas : Traitement des objets un par un
const feature_id = this.checkedFeatures[0]; // Récupère l'ID du premier objet sélectionné
const feature = this.clickedFeatures.find((el) => el.feature_id === feature_id); // Trouve l'objet complet
if (feature) { if (feature) {
featureAPI.updateFeature({ // Envoie une requête pour modifier le statut d'un objet spécifique
const response = await featureAPI.updateFeature({
feature_id, feature_id,
feature_type__slug: feature.feature_type, feature_type__slug: feature.feature_type,
project__slug: this.projectSlug, project__slug: this.projectSlug,
newStatus newStatus,
}).then((response) => {
if (response && response.data && response.status === 200) {
const newCheckedFeatures = [...this.checkedFeatures];
newCheckedFeatures.splice(this.checkedFeatures.indexOf(response.data.id), 1);
this.UPDATE_CHECKED_FEATURES(newCheckedFeatures);
this.modifyStatus(newStatus);
} else {
this.DISPLAY_MESSAGE({
comment: `Le signalement ${feature.title} n'a pas pu être modifié`,
level: 'negative'
});
this.fetchPagedFeatures();
}
}); });
if (response && response.data && response.status === 200) {
// Supprime l'objet traité de la liste des objets sélectionnés
const newCheckedFeatures = [...this.checkedFeatures];
newCheckedFeatures.splice(this.checkedFeatures.indexOf(response.data.id), 1);
this.UPDATE_CHECKED_FEATURES(newCheckedFeatures);
// Rappel récursif pour traiter l'objet suivant
this.modifyStatus(newStatus);
} else {
// Affiche un message d'erreur si la modification échoue
this.DISPLAY_MESSAGE({
comment: `Le signalement ${feature.title} n'a pas pu être modifié.`,
level: 'negative',
});
// Rafraîchit les données en cas d'erreur
this.fetchPagedFeatures();
}
} }
} else { } else {
this.fetchPagedFeatures(); // Cas : Tous les objets ont été traités après le traitement récursif
this.fetchPagedFeatures(); // Rafraîchit les données pour afficher les mises à jour
this.DISPLAY_MESSAGE({ this.DISPLAY_MESSAGE({
comment: 'Tous les signalements ont été modifié avec succès.', comment: 'Tous les signalements ont été modifiés avec succès.',
level: 'positive' level: 'positive',
}); });
} }
}, },
/**
* Supprime tous les objets sélectionnés.
*
* Cette méthode utilise `Promise.all` pour envoyer les requêtes de suppression en parallèle
* pour tous les objets dans la liste `checkedFeatures`. Après suppression, elle met à jour la pagination
* et rafraîchit les objets affichés pour refléter les changements.
*
* @returns {void}
*/
deleteAllFeatureSelection() { deleteAllFeatureSelection() {
// Sauvegarde le nombre total d'objets
const initialFeaturesCount = this.featuresCount; const initialFeaturesCount = this.featuresCount;
// Sauvegarde la page actuelle
const initialCurrentPage = this.pagination.currentPage; const initialCurrentPage = this.pagination.currentPage;
const promises = this.checkedFeatures.map( // Crée une liste de promesses pour supprimer chaque objet sélectionné
(feature_id) => this.DELETE_FEATURE({ feature_id, noFeatureType: true }) const promises = this.checkedFeatures.map((feature_id) =>
this.DELETE_FEATURE({ feature_id, noFeatureType: true })
); );
// Exécute toutes les suppressions en parallèle
Promise.all(promises) Promise.all(promises)
.then((response) => { .then((response) => {
const deletedFeaturesCount = response.reduce((acc, curr) => curr.status === 204 ? acc += 1 : acc, 0); // Compte le nombre d'objets supprimés avec succès
const deletedFeaturesCount = response.reduce(
(acc, curr) => (curr.status === 204 ? acc + 1 : acc),
0
);
// Calcule le nouveau total d'objets
const newFeaturesCount = initialFeaturesCount - deletedFeaturesCount; const newFeaturesCount = initialFeaturesCount - deletedFeaturesCount;
// Recalcule les pages
const newPagesArray = this.createPagesArray(newFeaturesCount, this.pagination.pagesize); const newPagesArray = this.createPagesArray(newFeaturesCount, this.pagination.pagesize);
// Dernière page valide
const newLastPageNum = newPagesArray[newPagesArray.length - 1]; const newLastPageNum = newPagesArray[newPagesArray.length - 1];
// Réinitialise la sélection
this.$store.commit('feature/UPDATE_CHECKED_FEATURES', []); this.$store.commit('feature/UPDATE_CHECKED_FEATURES', []);
if (initialCurrentPage > newLastPageNum) { //* if page doesn't exist anymore
this.toPage(newLastPageNum); //* go to new last page if (initialCurrentPage > newLastPageNum) {
// Navigue à la dernière page valide si la page actuelle n'existe plus
this.toPage(newLastPageNum);
} else { } else {
// Rafraîchit les objets affichés
this.fetchPagedFeatures(); this.fetchPagedFeatures();
} }
}) })
// Gère les erreurs éventuelles
.catch((err) => console.error(err)); .catch((err) => console.error(err));
// Ferme la modale de confirmation de suppression
this.toggleDeleteModal(); this.toggleDeleteModal();
}, },
...@@ -501,6 +601,14 @@ export default { ...@@ -501,6 +601,14 @@ export default {
this.fetchPagedFeatures(); this.fetchPagedFeatures();
}, },
handleAllSelectedChange(isChecked) {
this.allSelected = isChecked;
// Si des sélections existent, tout déselectionner
if (this.checkedFeatures.length > 0) {
this.UPDATE_CHECKED_FEATURES([]);
}
},
toPage(pageNumber) { toPage(pageNumber) {
const toAddOrRemove = const toAddOrRemove =
(pageNumber - this.pagination.currentPage) * this.pagination.pagesize; (pageNumber - this.pagination.currentPage) * this.pagination.pagesize;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment