loko/streetup/stock/static/stock/warehouses_index.js
2026-07-22 14:48:40 +02:00

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