feat(admin): Enregistre et utilise SortableReorder.js

Ajoute SortableReorder.js pour permettre le tri des lignes de tableau par drag-and-drop. L'implémente dans la page des formules.
```
This commit is contained in:
Serreau Jovann
2026-01-30 10:20:52 +01:00
parent b6f90721ad
commit b2928d896b
4 changed files with 186 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ import { SearchProduct, SearchOptions } from "./libs/SearchProduct.js";
import { SearchProductDevis, SearchOptionsDevis } from "./libs/SearchProductDevis.js";
import { SearchProductFormule, SearchOptionsFormule } from "./libs/SearchProductFormule.js";
import PlaningLogestics from "./libs/PlaningLogestics.js";
import {SortableReorder} from "./libs/SortableReorder.js";
// --- CONFIGURATION SENTRY ---
Sentry.init({
@@ -27,6 +28,7 @@ Sentry.init({
const registerCustomElements = () => {
const elements = [
{ name: 'repeat-line', class: RepeatLine, extends: 'div' },
{ name: 'sortable-reorder', class: SortableReorder, extends: 'table' },
{ name: 'devis-manager', class: DevisManager, extends: 'div' },
{ name: 'search-product', class: SearchProduct, extends: 'button' },
{ name: 'search-productformule', class: SearchProductFormule, extends: 'button' },

View File

@@ -0,0 +1,85 @@
import Sortable from 'sortablejs';
/**
* Composant <sortable-reorder url="/api/update-order">
* Encapsule un tableau pour rendre ses lignes triables.
*/
export class SortableReorder extends HTMLTableElement {
constructor() {
super();
this.sortable = null;
}
connectedCallback() {
// On attend que le DOM enfant soit parsé
setTimeout(() => this.init(), 0);
}
init() {
const tbody = this.querySelector('tbody');
const url = this.getAttribute('url');
if (!tbody || !url) {
console.warn('SortableReorder: <tbody> ou attribut "url" manquant.');
return;
}
this.sortable = new Sortable(tbody, {
animation: 150,
handle: '.cursor-move', // On attrape par l'icône uniquement
ghostClass: 'sortable-ghost', // Classe ajoutée à l'élément en mouvement (fond bleu)
onEnd: () => this.saveOrder(tbody, url)
});
}
async saveOrder(tbody, url) {
// Récupération des IDs dans le nouvel ordre
const items = Array.from(tbody.querySelectorAll('tr[data-id]'))
.map(tr => tr.dataset.id);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ items })
});
if (!response.ok) {
throw new Error('Erreur réseau lors de la sauvegarde.');
}
// Petit feedback visuel (optionnel)
this.showFeedback('success');
} catch (error) {
console.error('SortableReorder:', error);
this.showFeedback('error');
}
}
showFeedback(type) {
// Simple flash visuel sur le conteneur
const color = type === 'success' ? 'rgba(16, 185, 129, 0.2)' : 'rgba(239, 68, 68, 0.2)';
const originalTransition = this.style.transition;
this.style.transition = 'background-color 0.3s';
this.style.backgroundColor = color;
setTimeout(() => {
this.style.backgroundColor = 'transparent';
setTimeout(() => {
this.style.transition = originalTransition;
}, 300);
}, 500);
}
disconnectedCallback() {
if (this.sortable) {
this.sortable.destroy();
}
}
}