606 lines
21 KiB
JavaScript
606 lines
21 KiB
JavaScript
var warehouses_geojson = {};
|
|
|
|
const map = new maplibregl.Map({
|
|
container: "map",
|
|
style: window.MAP_STYLE_LIGHT,
|
|
center: [4.3572, 50.8477],
|
|
zoom: 11,
|
|
minzoom: 0,
|
|
maxzoom: 24
|
|
});
|
|
|
|
|
|
// Fonction pour charger les données GeoJSON
|
|
function loadGeoJSON(params = '') {
|
|
fetch(warehousesgeojsonUrl + `?${params}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
warehouses_geojson = data;
|
|
// Mettre à jour la carte
|
|
if (map.getSource('warehouses')) {
|
|
map.getSource('warehouses').setData(data);
|
|
} else {
|
|
map.addSource('warehouses', {
|
|
'type': 'geojson',
|
|
'data': data
|
|
});
|
|
map.addLayer({
|
|
'id': 'warehouses-clusters',
|
|
'type': 'circle',
|
|
'source': 'warehouses',
|
|
'paint': {
|
|
'circle-radius': {
|
|
'base': 5,
|
|
'stops': [
|
|
[12, 5],
|
|
[15, 10],
|
|
[22, 180]
|
|
]
|
|
},
|
|
'circle-color': '#0094ff',
|
|
'circle-opacity': 0.7
|
|
}
|
|
});
|
|
}
|
|
|
|
// Mettre à jour le tableau
|
|
loadDataIntoWarehousesTable(data.features);
|
|
|
|
});
|
|
}
|
|
|
|
// Charger les données initiales
|
|
map.on('load', async () => {
|
|
const params = new URLSearchParams(window.location.search).toString();
|
|
loadGeoJSON(params);
|
|
});
|
|
|
|
|
|
|
|
// Appliquer les filtres (si les éléments existent)
|
|
const applyFiltersBtn = document.getElementById('apply-filters');
|
|
if (applyFiltersBtn) {
|
|
applyFiltersBtn.addEventListener('click', () => {
|
|
const form = document.getElementById('filter-form');
|
|
const formData = new FormData(form);
|
|
const params = new URLSearchParams();
|
|
for (const [key, value] of formData.entries()) {
|
|
if (value.trim() !== '') { // Vérifie que le champ n'est pas vide
|
|
params.append(key, value);
|
|
}
|
|
}
|
|
|
|
// Mettre à jour l'URL
|
|
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
|
window.history.pushState({}, '', newUrl);
|
|
|
|
// Charger les données filtrées
|
|
loadGeoJSON(params.toString());
|
|
});
|
|
}
|
|
|
|
// Réinitialiser les filtres (si les éléments existent)
|
|
const resetFiltersBtn = document.getElementById('reset-filters');
|
|
if (resetFiltersBtn) {
|
|
resetFiltersBtn.addEventListener('click', () => {
|
|
const form = document.getElementById('filter-form');
|
|
form.reset(); // Réinitialiser les champs du formulaire
|
|
|
|
// Supprimer les paramètres de l'URL
|
|
window.history.pushState({}, '', window.location.pathname);
|
|
|
|
// Recharger toutes les données sans filtres
|
|
loadGeoJSON('');
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// Charger les données dans le tableau des entrepôts
|
|
function loadDataIntoWarehousesTable(features) {
|
|
const tableData = features.map((feature) => {
|
|
return {
|
|
actions: '', // Colonne pour l'icône, manipulée par le formatter
|
|
id: feature.id,
|
|
code: feature.properties.code,
|
|
name: feature.properties.name,
|
|
stock_magager: feature.properties.manager,
|
|
lon: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[0] : null, // Longitude
|
|
lat: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[1] : null // Latitude
|
|
};
|
|
});
|
|
|
|
$('#warehouses-spinner').hide();
|
|
$('#warehouses-table-container').show();
|
|
$('#warehouses-table').bootstrapTable('load', tableData);
|
|
}
|
|
|
|
|
|
// Ajouter les boutons d'action pour chaque ligne du tableau
|
|
function actionsFormatter(value, row, index) {
|
|
resp = `<button class="btn btn-sm btn-light detail-btn" data-id="${row.id}" data-bs-toggle="tooltip" title="Detail">
|
|
<i class="bi bi-info-circle"></i>
|
|
</button>`;
|
|
|
|
if (row.lon) {
|
|
resp = resp +
|
|
`<button class="btn btn-sm btn-light zoom-btn" data-lon="${row.lon}" data-lat="${row.lat}" data-bs-toggle="tooltip" title="Zoom">
|
|
<i class="bi bi-geo-alt"></i>
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
return resp;
|
|
}
|
|
|
|
$('#warehouses-table').on('click', '.zoom-btn', function () {
|
|
const lon = parseFloat($(this).data('lon'));
|
|
const lat = parseFloat($(this).data('lat'));
|
|
|
|
if (!isNaN(lon) && !isNaN(lat)) {
|
|
map.flyTo({
|
|
center: [lon, lat],
|
|
zoom: 16, // Niveau de zoom cible
|
|
essential: true // Pour rendre l'animation accessible
|
|
});
|
|
}
|
|
});
|
|
|
|
$('#warehouses-table').on('click', '.detail-btn', function () {
|
|
const featureid = $(this).data('id');
|
|
|
|
if (!isNaN(featureid)) {
|
|
window.open(warehousesUrl+featureid+"/","_self");
|
|
}
|
|
});
|
|
|
|
// Fonction pour formater la date
|
|
function dateFormatter(value, row, index) {
|
|
if (!value) return '-'; // Gérer les valeurs nulles ou indéfinies
|
|
const date = new Date(value);
|
|
const lang = (document.documentElement.lang || 'fr').split('-')[0];
|
|
const localeMap = { 'fr': 'fr-BE', 'nl': 'nl-BE', 'en': 'en-GB' };
|
|
const locale = localeMap[lang] || 'fr-BE';
|
|
return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
}
|
|
|
|
|
|
// Rafraîchir l'affichage du tableau
|
|
$('#warehouses-table-tab').on('click', function () {
|
|
$('#warehouses-table').bootstrapTable('resetView');
|
|
});
|
|
|
|
|
|
// Fonction pour mettre à jour le tableau avec les éléments contenus dans une zone délimitée
|
|
function filterTableByBounds(bounds, warehouses_data) {
|
|
|
|
const warehousesVisibleFeatures = warehouses_data.features.filter((feature) => {
|
|
if (feature.geometry && feature.geometry.type === 'Point') {
|
|
const [lon, lat] = feature.geometry.coordinates;
|
|
return bounds.contains([lon, lat]); // Vérifie si le point est dans les limites
|
|
}
|
|
return false; // Ignorer les autres types de géométries
|
|
});
|
|
loadDataIntoWarehousesTable(warehousesVisibleFeatures);
|
|
|
|
}
|
|
|
|
// Filtre sur la zone visible de la carte
|
|
$("#filterzone").on( "click", function(e) {
|
|
e.preventDefault();
|
|
filterTableByBounds(map.getBounds(), warehouses_geojson);
|
|
});
|
|
|
|
|
|
|
|
// Initialisation après le chargement de la page
|
|
$( document ).ready(function() {
|
|
|
|
function calculateTableHeight() {
|
|
const windowHeight = $(window).height(); // Hauteur totale de la fenêtre
|
|
const offsetTop = 285; // Position Y du tableau
|
|
const footerHeight = $('.footer').outerHeight() || 0; // Si vous avez un pied de page
|
|
return windowHeight - offsetTop - footerHeight - 20; // Ajustement avec un padding
|
|
}
|
|
|
|
$('#warehouses-table').bootstrapTable({
|
|
columns: [
|
|
{ field: 'actions', title: '', formatter:'actionsFormatter', width:'100' },
|
|
{ field: 'code', title: gettext('Code') },
|
|
{ field: 'name', title: gettext('Nom') },
|
|
{ field: 'stock_magager', title: gettext('Gestionnaire')}
|
|
],
|
|
height: calculateTableHeight() // Appliquer la hauteur calculée
|
|
});
|
|
|
|
$('#warehouse-locations-table').bootstrapTable();
|
|
|
|
|
|
// Appliquer la bonne largeur au dropdown-menu (si l'élément existe)
|
|
const filterDropdown = document.getElementById('filter-dropdown');
|
|
if (filterDropdown) {
|
|
const parent_filter_dropdown = $('#filter-dropdown').parent().parent()[0];
|
|
if (parent_filter_dropdown) {
|
|
const parentWidth = parent_filter_dropdown.offsetWidth;
|
|
$('#filter-dropdown').width(`${parentWidth}px`);
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
|
|
// ### Gestion des produits ### //
|
|
// ----------------------------- //
|
|
|
|
let productsData = [];
|
|
let currentPage = 1;
|
|
const itemsPerPage = 100;
|
|
let currentSort = { field: 'code', direction: 'asc' };
|
|
|
|
// Fonction pour charger les produits depuis l'API
|
|
function loadProducts(params = {}) {
|
|
$('#products-spinner').show();
|
|
$('#products-table-container').hide();
|
|
|
|
const queryParams = new URLSearchParams(params).toString();
|
|
const url = productsApiUrl + (queryParams ? `?${queryParams}` : '');
|
|
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
productsData = data;
|
|
currentPage = 1;
|
|
sortAndRenderProducts();
|
|
$('#products-spinner').hide();
|
|
$('#products-table-container').show();
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur lors du chargement des produits:', error);
|
|
$('#products-spinner').hide();
|
|
});
|
|
}
|
|
|
|
// Fonction de tri
|
|
function sortProducts(field) {
|
|
if (currentSort.field === field) {
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.field = field;
|
|
currentSort.direction = 'asc';
|
|
}
|
|
sortAndRenderProducts();
|
|
}
|
|
|
|
// Trier et afficher
|
|
function sortAndRenderProducts() {
|
|
// Tri des données
|
|
const sorted = [...productsData].sort((a, b) => {
|
|
const aVal = a[currentSort.field] || '';
|
|
const bVal = b[currentSort.field] || '';
|
|
|
|
if (currentSort.direction === 'asc') {
|
|
return aVal.toString().localeCompare(bVal.toString(), 'fr', { numeric: true });
|
|
} else {
|
|
return bVal.toString().localeCompare(aVal.toString(), 'fr', { numeric: true });
|
|
}
|
|
});
|
|
|
|
renderProductsTable(sorted);
|
|
updateSortIndicators();
|
|
}
|
|
|
|
// Formatter pour le prix
|
|
function formatPrice(value) {
|
|
if (value === null || value === undefined) return '-';
|
|
return new Intl.NumberFormat('fr-BE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(value);
|
|
}
|
|
|
|
// Afficher le tableau
|
|
function renderProductsTable(data) {
|
|
const tbody = document.getElementById('products-table-body');
|
|
const start = (currentPage - 1) * itemsPerPage;
|
|
const end = start + itemsPerPage;
|
|
const pageData = data.slice(start, end);
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
pageData.forEach(product => {
|
|
const row = document.createElement('tr');
|
|
const isInactive = product.is_active === false;
|
|
if (isInactive) {
|
|
row.className = 'table-light text-muted opacity-75';
|
|
row.setAttribute('style', 'opacity: 0.65; font-style: italic;');
|
|
}
|
|
|
|
// Construire l'URL de détail du produit
|
|
const productUrl = `/stock/products/${product.id}/`;
|
|
const totalStock = product.total_stock || 0;
|
|
const threshold = product.threshold || 0;
|
|
const stockColor = isInactive ? 'text-muted' : (totalStock < threshold ? 'text-danger fw-bold' : 'text-success fw-bold');
|
|
|
|
// Icone et badge pour produits inactifs
|
|
const icon = isInactive ? `<i class="bi bi-eye-slash text-muted me-1" title="${gettext('Inactif')}"></i>` : '';
|
|
const nameSuffix = isInactive ? ` <span class="badge bg-secondary text-white ms-2" style="font-size: 0.7rem; font-style: normal; font-weight: normal; vertical-align: middle;">${gettext('Inactif')}</span>` : '';
|
|
|
|
row.innerHTML = `
|
|
<td><a href="${productUrl}" class="text-decoration-none ${isInactive ? 'text-muted text-decoration-line-through' : ''}">${product.code || '-'}</a></td>
|
|
<td style="min-width: 200px;">${icon}${product.name || '-'}${nameSuffix}</td>
|
|
<td>${product.sku || '-'}</td>
|
|
<td>${product.category || '-'}</td>
|
|
<td class="text-end ${stockColor}">${totalStock}</td>
|
|
<td class="text-end">${threshold}</td>
|
|
<td class="text-end">${formatPrice(product.price)}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
renderPagination(data.length);
|
|
}
|
|
|
|
// Afficher la pagination
|
|
function renderPagination(totalItems) {
|
|
const pagination = document.getElementById('products-pagination');
|
|
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
|
|
|
pagination.innerHTML = '';
|
|
|
|
if (totalPages <= 1) return;
|
|
|
|
// Bouton Précédent
|
|
const prevLi = document.createElement('li');
|
|
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
|
prevLi.innerHTML = `<a class="page-link" href="#" data-page="${currentPage - 1}">Précédent</a>`;
|
|
pagination.appendChild(prevLi);
|
|
|
|
// Numéros de page
|
|
let startPage = Math.max(1, currentPage - 2);
|
|
let endPage = Math.min(totalPages, currentPage + 2);
|
|
|
|
if (startPage > 1) {
|
|
const firstLi = document.createElement('li');
|
|
firstLi.className = 'page-item';
|
|
firstLi.innerHTML = `<a class="page-link" href="#" data-page="1">1</a>`;
|
|
pagination.appendChild(firstLi);
|
|
|
|
if (startPage > 2) {
|
|
const dotsLi = document.createElement('li');
|
|
dotsLi.className = 'page-item disabled';
|
|
dotsLi.innerHTML = `<span class="page-link">...</span>`;
|
|
pagination.appendChild(dotsLi);
|
|
}
|
|
}
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
const li = document.createElement('li');
|
|
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
|
|
li.innerHTML = `<a class="page-link" href="#" data-page="${i}">${i}</a>`;
|
|
pagination.appendChild(li);
|
|
}
|
|
|
|
if (endPage < totalPages) {
|
|
if (endPage < totalPages - 1) {
|
|
const dotsLi = document.createElement('li');
|
|
dotsLi.className = 'page-item disabled';
|
|
dotsLi.innerHTML = `<span class="page-link">...</span>`;
|
|
pagination.appendChild(dotsLi);
|
|
}
|
|
|
|
const lastLi = document.createElement('li');
|
|
lastLi.className = 'page-item';
|
|
lastLi.innerHTML = `<a class="page-link" href="#" data-page="${totalPages}">${totalPages}</a>`;
|
|
pagination.appendChild(lastLi);
|
|
}
|
|
|
|
// Bouton Suivant
|
|
const nextLi = document.createElement('li');
|
|
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
|
nextLi.innerHTML = `<a class="page-link" href="#" data-page="${currentPage + 1}">Suivant</a>`;
|
|
pagination.appendChild(nextLi);
|
|
}
|
|
|
|
// Mettre à jour les indicateurs de tri
|
|
function updateSortIndicators() {
|
|
document.querySelectorAll('.sortable').forEach(th => {
|
|
const field = th.dataset.sort;
|
|
const arrows = th.querySelector('.sort-arrows');
|
|
|
|
if (!arrows) return;
|
|
|
|
const upArrow = arrows.querySelector('.sort-arrow.up');
|
|
const downArrow = arrows.querySelector('.sort-arrow.down');
|
|
|
|
// Réinitialiser
|
|
upArrow.classList.remove('active');
|
|
downArrow.classList.remove('active');
|
|
th.classList.remove('sort-active');
|
|
|
|
// Activer si c'est le champ actuel
|
|
if (field === currentSort.field) {
|
|
th.classList.add('sort-active');
|
|
if (currentSort.direction === 'asc') {
|
|
upArrow.classList.add('active');
|
|
} else {
|
|
downArrow.classList.add('active');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Event listeners pour le tri
|
|
document.addEventListener('click', (e) => {
|
|
const sortable = e.target.closest('.sortable');
|
|
if (sortable) {
|
|
const field = sortable.dataset.sort;
|
|
sortProducts(field);
|
|
}
|
|
});
|
|
|
|
// Event listeners pour la pagination
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.matches('.page-link')) {
|
|
e.preventDefault();
|
|
const page = parseInt(e.target.dataset.page);
|
|
if (page && page !== currentPage) {
|
|
currentPage = page;
|
|
sortAndRenderProducts();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Charger les produits au clic sur l'onglet
|
|
$('#products-table-tab').on('click', function () {
|
|
if (productsData.length === 0) {
|
|
const category = $('#products-category-filter').val();
|
|
const search = $('#products-search').val();
|
|
const include_inactive = $('#products-show-inactive').is(':checked');
|
|
loadProducts({ category, search, include_inactive });
|
|
}
|
|
});
|
|
|
|
// Filtrage automatique au changement de catégorie
|
|
$('#products-category-filter').on('change', function() {
|
|
const category = $(this).val();
|
|
const search = $('#products-search').val();
|
|
const include_inactive = $('#products-show-inactive').is(':checked');
|
|
loadProducts({ category, search, include_inactive });
|
|
});
|
|
|
|
// Filtrage automatique au changement de l'état "Afficher les inactifs"
|
|
$('#products-show-inactive').on('change', function() {
|
|
const include_inactive = $(this).is(':checked');
|
|
const category = $('#products-category-filter').val();
|
|
const search = $('#products-search').val();
|
|
loadProducts({ category, search, include_inactive });
|
|
});
|
|
|
|
// Recherche automatique après 2 caractères
|
|
let searchTimeout;
|
|
$('#products-search').on('input', function() {
|
|
const search = $(this).val();
|
|
const category = $('#products-category-filter').val();
|
|
const include_inactive = $('#products-show-inactive').is(':checked');
|
|
|
|
// Annuler le timeout précédent
|
|
clearTimeout(searchTimeout);
|
|
|
|
// Charger si 2+ caractères ou si vide (pour réinitialiser)
|
|
if (search.length >= 2 || search.length === 0) {
|
|
searchTimeout = setTimeout(() => {
|
|
loadProducts({ category, search, include_inactive });
|
|
}, 300); // Délai de 300ms pour éviter trop de requêtes
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// ### Actions sur la carte ### //
|
|
// -----------------------------//
|
|
|
|
const popup = new maplibregl.Popup({
|
|
closeButton: false,
|
|
closeOnClick: false,
|
|
anchor: 'bottom-left'
|
|
});
|
|
|
|
map.on('click', 'warehouses', async (e) => {
|
|
const currentZoom = map.getZoom();
|
|
|
|
const features = map.queryRenderedFeatures(e.point, {
|
|
layers: ['warehouses']
|
|
});
|
|
const center = features[0].geometry.coordinates;
|
|
|
|
if (currentZoom > 15) {
|
|
const { lng, lat } = e.lngLat; // Coordonnées cliquées
|
|
const distance = 10; // Distance en mètres pour chaque côté
|
|
|
|
// Convertir la distance en degrés géographiques
|
|
const latOffset = distance / 111320; // 1° latitude = ~111,320 m
|
|
const lngOffset = distance / (111320 * Math.cos((lat * Math.PI) / 180)); // Compte pour la latitude
|
|
|
|
|
|
const sw = new maplibregl.LngLat(lng - lngOffset, lat - latOffset);
|
|
const ne = new maplibregl.LngLat(lng + lngOffset, lat + latOffset);
|
|
const bounds = new maplibregl.LngLatBounds(sw, ne);
|
|
|
|
// Filtrer les données du tableau sur base de la zone
|
|
filterTableByBounds(bounds, warehouses_geojson);
|
|
|
|
} else {
|
|
const zoom = currentZoom + 2
|
|
map.easeTo({
|
|
center: center,
|
|
zoom
|
|
});
|
|
}
|
|
});
|
|
|
|
// Mouse pointer on clusters
|
|
map.on('mouseenter', 'warehouses', () => {
|
|
map.getCanvas().style.cursor = 'pointer';
|
|
});
|
|
map.on('mouseleave', 'warehouses', () => {
|
|
map.getCanvas().style.cursor = '';
|
|
});
|
|
|
|
// Layout switcher
|
|
function setupLayoutButtons() {
|
|
const btnTable = document.getElementById('layout-btn-table');
|
|
const btnBoth = document.getElementById('layout-btn-both');
|
|
const btnMap = document.getElementById('layout-btn-map');
|
|
const colLeft = document.getElementById('col-left');
|
|
const colRight = document.getElementById('col-right');
|
|
|
|
if (!btnTable || !btnBoth || !btnMap || !colLeft || !colRight) {
|
|
return;
|
|
}
|
|
|
|
function setLayout(layout) {
|
|
// Reset button states
|
|
[btnTable, btnBoth, btnMap].forEach(btn => btn.classList.remove('text-primary'));
|
|
|
|
if (layout === 'table_only') {
|
|
btnTable.classList.add('text-primary');
|
|
colLeft.classList.remove('d-none', 'col-md-6');
|
|
colLeft.classList.add('col-12');
|
|
colRight.classList.add('d-none');
|
|
} else if (layout === 'map_only') {
|
|
btnMap.classList.add('text-primary');
|
|
colLeft.classList.add('d-none');
|
|
colRight.classList.remove('d-none', 'col-md-6');
|
|
colRight.classList.add('col-12');
|
|
} else {
|
|
// both (default)
|
|
btnBoth.classList.add('text-primary');
|
|
colLeft.classList.remove('d-none', 'col-12');
|
|
colLeft.classList.add('col-md-6');
|
|
colRight.classList.remove('d-none', 'col-12');
|
|
colRight.classList.add('col-md-6');
|
|
}
|
|
|
|
// Save preference in localStorage
|
|
localStorage.setItem('warehouses_layout', layout);
|
|
|
|
// Resize map and table
|
|
setTimeout(() => {
|
|
map?.resize();
|
|
$('#warehouses-table').bootstrapTable('resetView');
|
|
$('#warehouse-locations-table').bootstrapTable('resetView');
|
|
}, 100);
|
|
}
|
|
|
|
btnTable.addEventListener('click', () => setLayout('table_only'));
|
|
btnBoth.addEventListener('click', () => setLayout('both'));
|
|
btnMap.addEventListener('click', () => setLayout('map_only'));
|
|
|
|
// Restore saved layout
|
|
const savedLayout = localStorage.getItem('warehouses_layout') || 'both';
|
|
setLayout(savedLayout);
|
|
}
|
|
|
|
// Initialize layout buttons on page load
|
|
document.addEventListener('DOMContentLoaded', setupLayoutButtons);
|
|
|