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

1508 lines
No EOL
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var visible_locations = []
var visible_assets = []
var filter_geom = ''
var locations_geojson = {};
const locationsgeojsonloader = new GeojsonLoader();
if (!window.locations_outline_layer) {
locations_outline_layer = {
id: 'locations-outline',
type: 'line',
source: 'locations',
paint: {
'line-color': [
'case',
['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b',
'#00000000' // Transparent si pas highlighté
],
'line-width': [
'case',
['boolean', ['feature-state', 'highlighted'], false], 1,
0
]
}
};
}
handleGeojsonMapLoading(init=true, locationsgeojsonloader, 'locations', [locations_layer_def,locations_outline_layer]);
// Reset table if error occurs when loading GeoJSON
locationsgeojsonloader.addEventListener("geojsonError", event => {
loadDataIntoLocationsTable([]);
});
// Load data into table when GeoJSON is loaded
locationsgeojsonloader.addEventListener("geojsonLoaded", event => {
var geojson_data = event.detail;
loadDataIntoLocationsTable(geojson_data.features);
locations_geojson = geojson_data;
});
var assets_geojson = {};
const assetsgeojsonloader = new GeojsonLoader();
handleGeojsonMapLoading(init=true, assetsgeojsonloader, 'assets', assets_layers_defs);
assetsgeojsonloader.addEventListener("geojsonError", event => {
loadDataIntoAssetsTable([]);
});
assetsgeojsonloader.addEventListener("geojsonLoaded", event => {
var geojson_data = event.detail;
loadDataIntoAssetsTable(geojson_data.features);
assets_geojson = geojson_data;
});
// Load Map
maploader.loadMap("map");
// Drawing selection tool
MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas';
MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl';
MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-';
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';
MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib';
const drawstyles=[{id:"gl-draw-polygon-fill",type:"fill",filter:["all",["==","$type","Polygon"],],paint:{"fill-color":["case",["==",["get","active"],"true"],"orange","blue",],"fill-opacity":["case",["==",["get","active"],"true"],0.1,0,]}},{id:"gl-draw-lines",type:"line",filter:["any",["==","$type","LineString"],["==","$type","Polygon"],],layout:{"line-cap":"round","line-join":"round"},paint:{"line-color":["case",["==",["get","active"],"true"],"orange","blue",],"line-dasharray":["literal",[.2,2]],"line-width":2}},{id:"gl-draw-point-outer",type:"circle",filter:["all",["==","$type","Point"],["==","meta","feature"],],paint:{"circle-radius":["case",["==",["get","active"],"true"],7,5,],"circle-color":"white"}},{id:"gl-draw-point-inner",type:"circle",filter:["all",["==","$type","Point"],["==","meta","feature"],],paint:{"circle-radius":["case",["==",["get","active"],"true"],5,3,],"circle-color":["case",["==",["get","active"],"true"],"orange","blue",]}},{id:"gl-draw-vertex-outer",type:"circle",filter:["all",["==","$type","Point"],["==","meta","vertex"],["!=","mode","simple_select"],],paint:{"circle-radius":["case",["==",["get","active"],"true"],7,5,],"circle-color":"white"}},{id:"gl-draw-vertex-inner",type:"circle",filter:["all",["==","$type","Point"],["==","meta","vertex"],["!=","mode","simple_select"],],paint:{"circle-radius":["case",["==",["get","active"],"true"],5,3,],"circle-color":"orange"}},{id:"gl-draw-midpoint",type:"circle",filter:["all",["==","meta","midpoint"],],paint:{"circle-radius":3,"circle-color":"orange"}},];
const draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
polygon: true,
trash: true
},
styles: drawstyles
});
map.on('load', () => {
map.addControl(draw);
map.boxZoom.enable();
// Initial state
updateAutoZoneVisibility();
onMoveEndAuto(); // if already zoomed in on load, run once
// Gestion du curseur pour les vertices et midpoints
map.on('mousemove', (e) => {
const canvas = map.getCanvas();
// Récupérer les layers draw en utilisant des préfixes (car ils ont des suffixes .cold/.hot)
const allLayers = map.getStyle().layers;
const drawLayerPrefixes = ['gl-draw-polygon-fill', 'gl-draw-vertex-outer', 'gl-draw-midpoint'];
const existingLayers = allLayers
.filter(layer => drawLayerPrefixes.some(prefix => layer.id.startsWith(prefix)))
.map(layer => layer.id);
if (existingLayers.length === 0) return;
const features = map.queryRenderedFeatures(e.point, {
layers: existingLayers
});
// Variable pour savoir si on a appliqué un curseur draw
let drawCursorApplied = false;
if (features.length > 0) {
const feature = features[0];
// Si c'est un vertex ou midpoint
if (feature.properties.meta === 'vertex' || feature.properties.meta === 'midpoint') {
canvas.style.cursor = 'grab';
drawCursorApplied = true;
}
// Si c'est le polygone actif en mode direct_select
else if (feature.properties.active === 'true') {
canvas.style.cursor = 'move';
drawCursorApplied = true;
}
// Si c'est un polygone non activé (sélectionnable)
else if (feature.properties.active === 'false') {
canvas.style.cursor = 'pointer';
drawCursorApplied = true;
}
}
// Réinitialiser le curseur si on ne survole plus un élément draw
// et qu'on n'est pas en mode dessin
if (!drawCursorApplied) {
const currentMode = draw.getMode();
if (currentMode === 'draw_polygon') {
canvas.style.cursor = 'crosshair';
} else if (currentMode === 'simple_select' || currentMode === 'direct_select') {
// Ne pas interférer avec les curseurs des autres couches
// Réinitialiser uniquement si aucun autre événement ne gère le curseur
const otherFeatures = map.queryRenderedFeatures(e.point);
const hasOtherFeatures = otherFeatures.some(f =>
!existingLayers.includes(f.layer.id)
);
if (!hasOtherFeatures) {
canvas.style.cursor = '';
}
}
}
});
// Curseur pendant le drag d'un vertex
map.on('mousedown', (e) => {
const allLayers = map.getStyle().layers;
const drawLayerPrefixes = ['gl-draw-vertex-outer', 'gl-draw-midpoint'];
const existingLayers = allLayers
.filter(layer => drawLayerPrefixes.some(prefix => layer.id.startsWith(prefix)))
.map(layer => layer.id);
if (existingLayers.length === 0) return;
const features = map.queryRenderedFeatures(e.point, {
layers: existingLayers
});
if (features.length > 0) {
map.getCanvas().style.cursor = 'grabbing';
}
});
map.on('mouseup', () => {
const currentMode = draw.getMode();
if (currentMode === 'direct_select') {
map.getCanvas().style.cursor = 'move';
}
});
});
map.on('draw.create', (e) => {
const features = draw.getAll().features;
if (features.length > 1) {
// Supprime tous les autres sauf le dernier
const keep = e.features[0].id;
const idsToDelete = features
.filter(f => f.id !== keep)
.map(f => f.id);
draw.delete(idsToDelete);
}
// Réinitialiser le curseur après la création
map.getCanvas().style.cursor = '';
});
map.on('dblclick', (e) => {
e.preventDefault();
// Parfois le dragPan est désactivé à tort, on le réactive manuellement
if (!map.dragPan.isEnabled()) {
map.dragPan.enable();
}
});
map.on('draw.modechange', (e) => {
const safeModes = ['simple_select', 'direct_select'];
if (safeModes.includes(e.mode) && !map.boxZoom.isEnabled()) {
map.boxZoom.enable();
}
// Changer le curseur en fonction du mode
const canvas = map.getCanvas();
if (e.mode === 'draw_polygon') {
canvas.style.cursor = 'crosshair';
} else if (e.mode === 'direct_select') {
canvas.style.cursor = 'move';
} else {
canvas.style.cursor = '';
}
});
map.on('styledata', () => {
if (map.getLayer('assets') && map.getLayer('locations')) {
map.moveLayer('locations', 'assets');
}
});
// Récupération des IDs des layers 'assets'
const assetLayerIds = assets_layers_defs.map(l => l.id);
// Map layers events
//registerLayerMouseEnter(map, 'locations', popup, 'code', 14);
registerLayerMouseMove(map, 'locations', popup, 'code', 14);
registerLayerMouseLeave(map, 'locations', popup);
assetLayerIds.forEach(id => {
registerLayerMouseMove(map, id, popup, 'name', 17);
registerLayerMouseLeave(map, id, popup);
});
var locationshighlightState = { previousId: null }; // Track the previously highlighted feature
map.on('click', 'locations', (e) => {
const featureId = e.features[0].id;
const currentZoom = map.getZoom();
if (currentZoom > 13) {
// Activer l'onglet "locations" pour afficher le tableau
const locationsTab = document.getElementById('locations-table-tab');
if (locationsTab) {
const tab = new bootstrap.Tab(locationsTab);
tab.show();
}
// Highlight the corresponding row in the Bootstrap table
highlightRow('#locations-table', 'id', featureId);
// Update feature highlight on the map
highlightMapFeature(map, 'locations', featureId, locationshighlightState);
// Load assets GeoJSON for the selected location
// const locationId = featureId;
// const streetAssetsGeojsonUrl = `${assetsgeojsonUrl}?location=${locationId}`;
// assetsgeojsonloader.loadGeojson(streetAssetsGeojsonUrl);
// handleGeojsonMapLoading(init=false, assetsgeojsonloader, 'assets', [assets_layer_def]);
}
});
var assetshighlightState = { previousId: null }; // Track the previously highlighted feature
// Handler unique pour tous les layers "assets"
function onAssetClick(e) {
const f = e.features && e.features[0];
if (!f) return;
// Id robuste (si jamais .id n’est pas présent)
const featureId =
f.id ??
f.properties?.id ??
f.properties?.pk ??
f.properties?.objectid;
if (featureId == null) return;
if (map.getZoom() > 13) {
// Activer l'onglet "assets" pour afficher le tableau
const assetsTab = document.getElementById('assets-table-tab');
if (assetsTab) {
const tab = new bootstrap.Tab(assetsTab);
tab.show();
}
// surbrillance dans le tableau
highlightRow('#assets-table', 'id', featureId);
// surbrillance sur la carte
highlightMapFeature(map, 'assets', featureId, assetshighlightState);
}
}
// Enregistrement du handler sur chaque layer + curseur
assetLayerIds.forEach(id => {
map.on('click', id, onAssetClick);
map.on('mouseenter', id, () => (map.getCanvas().style.cursor = 'pointer'));
map.on('mouseleave', id, () => (map.getCanvas().style.cursor = ''));
});
// === config & state ===
const AUTO_ZOOM_THRESHOLD = 15;
const autoZoneWrapper = document.getElementById('auto-zone-wrapper');
const autoZoneCheck = document.getElementById('autoZoneCheck');
let autoZoneEnabled = true;
let lastBboxKey = null;
// mémoriser si on était au-dessus du seuil
let prevIsHighZoom = map.getZoom() >= AUTO_ZOOM_THRESHOLD;
function debounce(fn, wait=300) {
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
}
// Make a deterministic key for current bbox (rounded to 6 decimals)
function bboxKey(bounds) {
const sw = bounds.getSouthWest(), ne = bounds.getNorthEast();
const r = n => n.toFixed(6);
return `${r(sw.lng)},${r(sw.lat)}|${r(ne.lng)},${r(ne.lat)}`;
}
function bboxFeatureFromBounds(bounds) {
const sw = bounds.getSouthWest();
const ne = bounds.getNorthEast();
const nw = { lng: sw.lng, lat: ne.lat };
const se = { lng: ne.lng, lat: sw.lat };
return {
type: "Feature",
properties: { source: "viewbbox" },
geometry: { type: "Polygon", coordinates: [[
[sw.lng, sw.lat],[se.lng, se.lat],[ne.lng, ne.lat],[nw.lng, nw.lat],[sw.lng, sw.lat]
]]}
};
}
function updateAutoZoneVisibility() {
const isHigh = map.getZoom() >= AUTO_ZOOM_THRESHOLD;
// Affiche/masque la checkbox
if (isHigh) {
autoZoneWrapper.classList.remove('d-none');
} else {
autoZoneWrapper.classList.add('d-none');
}
// ⬇️ NEW: si on vient de passer de ≥ seuil à < seuil → reset ONCE
if (prevIsHighZoom && !isHigh) {
lastBboxKey = null; // invalide la clé de bbox
resetMapFilter(); // appelle ton reset exactement une fois sur la transition
}
prevIsHighZoom = isHigh; // maj l'état pour la prochaine fois
}
const triggerAutoZoneFilter = () => {
const geomFeature = bboxFeatureFromBounds(map.getBounds());
updateSelection(geomFeature); // your existing function
};
// Single place that triggers the filter
const onMoveEndAuto = debounce(() => {
if (!autoZoneEnabled) return;
if (map.getZoom() < AUTO_ZOOM_THRESHOLD) return;
const key = bboxKey(map.getBounds());
if (key !== lastBboxKey) {
lastBboxKey = key;
triggerAutoZoneFilter();
}
}, 250);
// Wire events
map.on('zoomend', updateAutoZoneVisibility); // visibility only
map.on('moveend', onMoveEndAuto);
autoZoneCheck.addEventListener('change', (e) => {
autoZoneEnabled = e.target.checked;
lastBboxKey = null; // ensure a fresh trigger on next move
if (autoZoneEnabled && map.getZoom() >= AUTO_ZOOM_THRESHOLD) {
onMoveEndAuto(); // schedule one run
}
if (!autoZoneEnabled) {
resetMapFilter();
}
});
document.addEventListener("DOMContentLoaded", () => {
const autoZoneCheck = document.getElementById("autoZoneCheck");
if (autoZoneCheck) {
autoZoneCheck.checked = true;
}
});
// Charger les données initiales
let params = new URLSearchParams(window.location.search).toString();
locationsgeojsonloader.loadGeojson(locationsgeojsonUrl+`?${params}`);
assetsgeojsonloader.loadGeojson(assetsgeojsonUrl+`?${params}`);
function actionsFormatter(value, row, index) {
if (selected_locations_set.has(row.id)) {
icon_select = 'bi-check-square-fill';
} else {
icon_select = 'bi-square';
}
resp = `<button class="btn btn-sm btn-light location-btn-toggle" data-id="${row.id}" data-bs-toggle="tooltip" title="(Dé)sélectionner la localisation">
<i class="bi ${icon_select}"></i>
</button>`;
if (row.lon) {
resp = resp +
`<button class="btn btn-sm btn-light zoom-btn ms-1" data-lon="${row.lon}" data-lat="${row.lat}" data-bs-toggle="tooltip" title="Zoom">
<i class="bi bi-geo-alt"></i>
</button>`;
}
return resp;
}
function codeFormatterLocations(value, row, index) {
if (row.detail_url) {
return `<a href="${row.detail_url}" class="text-decoration-none">${value}</a>`;
}
return value;
}
function actionsFormatterAssets(value, row, index) {
const key = assetKey(row.asset_type, row.id);
const isSelected = selected_assets_set.has(key);
const btnClass = 'btn-light';
const iconClass = isSelected ? 'bi-check-square' : 'bi-square';
const selectedValue = isSelected ? 'true' : 'false';
resp = `
<button class="btn btn-sm ${btnClass} asset-btn-toggle"
data-id="${row.id}"
data-asset_type="${row.asset_type}"
data-key="${key}"
data-selected="${selectedValue}"
data-bs-toggle="tooltip"
title="${isSelected ? "Désélectionner l'asset" : "Sélectionner l'asset"}">
<i class="bi ${iconClass}"></i>
</button>`;
if (row.lon) {
resp = resp +
`<button class="btn btn-sm btn-light zoom-btn ms-1" data-lon="${row.lon}" data-lat="${row.lat}" data-bs-toggle="tooltip" title="Zoom">
<i class="bi bi-geo-alt"></i>
</button>`;
}
return resp;
}
function codeFormatterAssets(value, row, index) {
if (row.detail_url) {
return `<a href="${row.detail_url}" class="text-decoration-none">${value}</a>`;
}
return value;
}
// Loads features into the locations table
function loadDataIntoLocationsTable(features) {
const tableData = features.map((feature) => {
const row = {
actions: '', // Colonne pour les actions (boutons, icônes, etc.)
id: feature.id,
code: feature.properties.code,
lon: feature.properties.lon,
lat: feature.properties.lat
};
// Ajoute dynamiquement les autre propriétés
locations_property_fields.forEach(field => {
row[field] = feature.properties[field];
});
row.detail_url = feature.properties.detail_url || null;
return row;
});
$('#locations-spinner').hide();
$('#locations-table-container').show();
$('#locations-table').bootstrapTable('load', tableData);
}
// Loads features into the assets table
function loadDataIntoAssetsTable(features) {
const tableData = features.map((feature) => {
const row = {
actions: '', // Colonne pour les actions (boutons, icônes, etc.)
id: feature.id,
asset_type: feature.properties.asset_type,
code: feature.properties.code,
status: '<i class="bi bi-circle-fill status_'+feature.properties.status+'"></i>',
lon: feature.properties.lon,
lat: feature.properties.lat
};
// Ajoute dynamiquement les autre propriétés
assets_property_fields.forEach(field => {
row[field] = feature.properties[field];
});
row.detail_url = feature.properties.detail_url || null;
return row;
});
$('#assets-spinner').hide();
$('#assets-table-container').show();
$('#assets-table').bootstrapTable('load', tableData);
}
function redirectToThematic() {
const select = document.getElementById('theme-select');
const selectedValue = select.value;
if (selectedValue) {
// Rediriger vers l'URL sélectionnée
window.location.href = selectedValue;
}
}
$( document ).ready(function() {
// Apply the correct width to the dropdown-menu
const parent_filter_dropdown = $('#col-table');
const parentWidth = parent_filter_dropdown.width() - 20;
// $('#filter-dropdown-wrapper').find('.dropdown-menu').width(`${parentWidth}px`);
function calculateTableHeight() {
const windowHeight = $(window).height(); // Total window height
const offsetTop = 285; // Y position of the table
const footerHeight = $('.footer').outerHeight() || 0;
return windowHeight - offsetTop - footerHeight - 20;
}
$('#locations-table').bootstrapTable({
columns: [
{ field: 'actions', title: '', formatter:'actionsFormatter', class: 'text-nowrap', width: 90 },
{ field: 'code', title: gettext('Code'), class: 'text-nowrap', formatter: 'codeFormatterLocations' },
...locations_columns // Colonnes spécifiques
],
height: calculateTableHeight(), // Apply computed height
onClickRow: function (row, $element) {
// Remove highlight from all previously highlighted cells
$('#locations-table').find('tbody td').removeClass('rowhighlight');
// Apply the highlight class to all `<td>` cells in the row
$element.find('td').addClass('rowhighlight');
// Update feature highlight on the map
highlightMapFeature(map, 'locations', row.id, locationshighlightState);
// Load assets GeoJSON for the selected location
// const locationId = row.id;
// const locationAssetsGeojsonUrl = `${assetsgeojsonUrl}?location=${locationId}`;
// assetsgeojsonloader.loadGeojson(locationAssetsGeojsonUrl);
// handleGeojsonMapLoading(init=false, assetsgeojsonloader, 'assets', [assets_layer_def]);
}
});
$('#assets-table').bootstrapTable({
columns: [
{ field: 'actions', title: '', formatter:'actionsFormatterAssets', width: 90 },
{ field: 'code', title: gettext('Code'), formatter: 'codeFormatterAssets' },
...assets_columns, // Colonnes spécifiques
{ field: 'status', title: gettext('Statut'), align: 'center' }
],
height: calculateTableHeight(), // Apply computed height
onClickRow: function (row, $element) {
// Remove highlight from all previously highlighted cells
$('#assets-table').find('tbody td').removeClass('rowhighlight');
// Apply the highlight class to all `<td>` cells in the row
$element.find('td').addClass('rowhighlight');
// Update feature highlight on the map
highlightMapFeature(map, 'assets', row.id, assetshighlightState);
}
});
$('#theme-select').on('change', function () {
redirectToThematic();
});
// reset view when tab is clicked (in case of window resize)
$('#locations-table-tab').on('click', function () {
$('#locations-table').bootstrapTable('resetView');
});
$('#assets-table-tab').on('click', function () {
$('#assets-table').bootstrapTable('resetView');
});
// Apply filters
document.getElementById('apply-filters').addEventListener('click', function(e) {
// 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);
// }
// }
// // Update URL
// const newUrl = `${window.location.pathname}?${params.toString()}`;
// window.history.pushState({}, '', newUrl);
// Load filtered data
// handleGeojsonMapLoading(init=false, locationsgeojsonloader, 'locations', [locations_layer_def]);
// locationsgeojsonloader.loadGeojson(locationsgeojsonUrl+`?${params.toString()}`);
updateLocations();
// Trouver le dropdown parent et le refermer
const dropdownEl = this.closest('.dropdown');
const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]'));
if (dropdown) {
dropdown.hide(); // ✅ ferme proprement le dropdown
}
});
// Reset filters
document.getElementById('reset-filters').addEventListener('click', function(e) {
const form = document.getElementById('filter-form');
//form.reset(); // Reset form fields
// Parcourir tous les champs du formulaire et les réinitialiser
form.querySelectorAll('input, select, textarea').forEach(field => {
if (field.tagName.toLowerCase() === 'select') {
field.selectedIndex = -1; // aucune option sélectionnée
} else if (field.type === 'checkbox' || field.type === 'radio') {
field.checked = false; // décocher
} else {
field.value = ''; // vider le texte / nombre / date
}
});
// // Remove URL parameters
// window.history.pushState({}, '', window.location.pathname);
// Reload all data without filters
// handleGeojsonMapLoading(init=false, locationsgeojsonloader, 'locations', [locations_layer_def]);
// locationsgeojsonloader.loadGeojson(locationsgeojsonUrl);
updateLocations();
// Trouver le dropdown parent et le refermer
const dropdownEl = this.closest('.dropdown');
const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]'));
if (dropdown) {
dropdown.hide(); // ✅ ferme proprement le dropdown
}
});
// Apply assets filters
document.getElementById('apply-assets-filters').addEventListener('click', function(e) {
// Load filtered data
updateAssets();
// Trouver le dropdown parent et le refermer
const dropdownEl = this.closest('.dropdown');
const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]'));
if (dropdown) {
dropdown.hide(); // ✅ ferme proprement le dropdown
}
});
// Reset filters
document.getElementById('reset-assets-filters').addEventListener('click', function(e) {
const form = document.getElementById('assets-filter-form');
// form.reset(); // Reset form fields
// Parcourir tous les champs du formulaire et les réinitialiser
form.querySelectorAll('input, select, textarea').forEach(field => {
if (field.tagName.toLowerCase() === 'select') {
field.selectedIndex = -1; // aucune option sélectionnée
} else if (field.type === 'checkbox' || field.type === 'radio') {
field.checked = false; // décocher
} else {
field.value = ''; // vider le texte / nombre / date
}
});
updateAssets();
// Trouver le dropdown parent et le refermer
const dropdownEl = this.closest('.dropdown');
const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]'));
if (dropdown) {
dropdown.hide(); // ✅ ferme proprement le dropdown
}
});
// Filter tables by map bounds
// $("#filterzone").on( "click", function(e) {
// e.preventDefault();
// filterTableByMapBounds(map, locations_geojson, loadDataIntoLocationsTable);
// filterTableByMapBounds(map, assets_geojson, loadDataIntoAssetsTable);
// });
// Register zoom action to the tables
registerZoomAction('#locations-table', '.zoom-btn', map, 17);
registerZoomAction('#assets-table', '.zoom-btn', map, 18);
// Register location selection action to table locations
registerSelectLocationAction('#locations-table', '.location-btn-toggle');
});
function registerSelectLocationAction(tableId, buttonClass) {
$(tableId).on('click', buttonClass, function () {
const $button = $(this);
const locationId = $(this).data('id'); // Retrieve the ID from the button's data attribute
if (locationId) {
toggleSelectedLocation(locationId, $button);
}
});
}
function toggleSelectedLocation(locationId, $button) {
if (selected_locations_set.has(locationId)) {
selected_locations_set.delete(locationId);
$button
.find('i').removeClass('bi-check-square-fill').addClass('bi-square');
} else {
selected_locations_set.add(locationId);
$button
.find('i').removeClass('bi-square').addClass('bi-check-square-fill');
}
updateSelectedLocationsInputAndStatus();
if (preventive_draft_id !== null && preventive_draft_id !== 'None' && preventive_draft_id !== '' && preventive_element_type == 'locations') {
// not necessary to search assets if it concerns a selection for preventive interventions
} else {
updateAssets();
}
}
function updateSelectedLocationsInputAndStatus() {
const previnputField = document.getElementById("input-preventive-selected-locations-ids");
const statusBar = document.getElementById("locations-selection-status");
const ids = Array.from(selected_locations_set);
if (previnputField) previnputField.value = ids.join(",");
statusBar.innerText = interpolate(
ngettext('%(count)s localisation sélectionnée',
'%(count)s localisations sélectionnées',
ids.length),
{ count: ids.length },
true
);
}
function updateSelectedAssetsInputAndStatus() {
const statusBar = document.getElementById("assets-selection-status");
const assets = Array.from(selected_assets_set);
statusBar.innerText = interpolate(
ngettext('%(count)s asset sélectionné',
'%(count)s assets sélectionnés',
assets.length),
{ count: assets.length },
true
);
}
// Selection of locations
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("locations-select-all").addEventListener("click", () => {
const allRows = $('#locations-table').bootstrapTable('getData');
allRows.forEach(row => {
selected_locations_set.add(parseInt(row.id));
const btn = document.querySelector(`.location-btn-toggle[data-id="${row.id}"]`);
if (btn) {
$(btn).find('i').removeClass('bi-square').addClass('bi-check-square-fill');
}
});
updateSelectedLocationsInputAndStatus();
if (preventive_draft_id !== null && preventive_draft_id !== 'None' && preventive_draft_id !== '' && preventive_element_type == 'locations') {
// not necessary to search assets if it concerns a selection for preventive interventions
} else {
updateAssets();
}
});
document.getElementById("locations-deselect-all").addEventListener("click", () => {
selected_locations_set.clear();
const allRows = $('#locations-table').bootstrapTable('getData');
allRows.forEach(row => {
const btn = document.querySelector(`.location-btn-toggle[data-id="${row.id}"]`);
if (btn) {
$(btn).find('i').removeClass('bi-check-square-fill').addClass('bi-square');
}
});
updateSelectedLocationsInputAndStatus();
if (preventive_draft_id !== null && preventive_draft_id !== 'None' && preventive_draft_id !== '' && preventive_element_type == 'locations') {
// not necessary to search assets if it concerns a selection for preventive interventions
} else {
updateAssets();
}
});
// Initial state
updateSelectedLocationsInputAndStatus();
document.getElementById("assets-select-all").addEventListener("click", () => {
const allRows = $('#assets-table').bootstrapTable('getData');
console.log(allRows);
allRows.forEach(row => {
selected_assets_set.add(assetKey(row.asset_type, row.id));
const btn = document.querySelector(`.asset-btn-toggle[data-id="${row.id}"]`);
if (btn) {
$(btn).find('i').removeClass('bi-square').addClass('bi-check-square');
}
});
updateSelectedAssetsInputAndStatus();
});
document.getElementById("assets-deselect-all").addEventListener("click", () => {
selected_assets_set.clear();
const allRows = $('#assets-table').bootstrapTable('getData');
allRows.forEach(row => {
const btn = document.querySelector(`.asset-btn-toggle[data-id="${row.id}"]`);
if (btn) {
$(btn).find('i').removeClass('bi-check-square').addClass('bi-square');
}
});
updateSelectedAssetsInputAndStatus();
});
// Initial state
updateSelectedAssetsInputAndStatus();
});
document.getElementById("assets-table").addEventListener('click', (e) => {
const btn = e.target.closest('.asset-btn-toggle');
if (!btn) return;
const key = btn.dataset.key;
const icon = btn.querySelector('i');
if (selected_assets_set.has(key)) {
selected_assets_set.delete(key);
btn.dataset.selected = 'false';
icon.classList.remove('bi-check-square');
icon.classList.add('bi-square');
btn.title = gettext("Sélectionner l'asset");
} else {
selected_assets_set.add(key);
btn.dataset.selected = 'true';
icon.classList.remove('bi-square');
icon.classList.add('bi-check-square');
btn.title = gettext("Désélectionner l'asset");
}
updateSelectedAssetsInputAndStatus();
});
// Avant soumission, sérialiser le Set -> JSON dans le champ caché
if (document.getElementById('preventive-select-form')) {
document.getElementById('preventive-select-form').addEventListener('submit', () => {
const assets = Array.from(selected_assets_set).map(k => {
const [model, id] = k.split(':');
return { model, object_id: Number(id) };
});
document.getElementById('input-preventive-selected-assets').value = JSON.stringify(assets);
});
}
// Positionnne le filtre dans la toolbar du tableau
document.addEventListener("DOMContentLoaded", function () {
$('#locations-table').on('post-body.bs.table', function () {
const $toolbar = $('#locations-table-container').find('.bootstrap-table').find('.fixed-table-toolbar');
// Ne pas réinsérer si déjà fait
if ($toolbar.find('#filter-dropdown-wrapper').length === 0) {
$('#filter-dropdown-wrapper')
.removeClass('d-none')
.prependTo($toolbar);
}
});
$('#assets-table').on('post-body.bs.table', function () {
const $toolbar = $('#assets-table-container').find('.bootstrap-table').find('.fixed-table-toolbar');
// Ne pas réinsérer si déjà fait
if ($toolbar.find('#filter-assets-dropdown-wrapper').length === 0) {
$('#filter-assets-dropdown-wrapper')
.removeClass('d-none')
.prependTo($toolbar);
}
});
});
function updateLocations() {
const alertBox = document.getElementById("location-alert");
const form = document.getElementById('filter-form');
const formData = new FormData(form);
// Update URL
if (preventive_draft_id == 'None') {
updateURLFromForms('filter-form', 'assets-filter-form');
}
// On convertit FormData en objet
const filters = {};
for (const [key, value] of formData.entries()) {
if (value.trim() !== '') {
// Si le champ existe déjà (checkbox, multiple select), on stocke en tableau
if (filters[key]) {
if (!Array.isArray(filters[key])) {
filters[key] = [filters[key]];
}
filters[key].push(value);
} else {
filters[key] = value;
}
}
}
try {
parsedGeom = JSON.parse(filter_geom);
} catch (e) {
parsedGeom = null;
}
fetch(locationsgeojsonUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken') // si CSRF est activé
},
body: JSON.stringify({
filter_geom: parsedGeom, // Ajouter la géométrie
q: (document.getElementById('assets-search-input') ? document.getElementById('assets-search-input').value.trim() : ''),
search_mode: (document.querySelector('input[name="assets-search-mode"]:checked') ? document.querySelector('input[name="assets-search-mode"]:checked').value : 'and'),
...filters // Ajout de tous les champs de filtre
})
})
.then(response => response.json())
.then(geojsonData => {
if (geojsonData.error) {
// Affiche l'erreur dans le div Bootstrap
alertBox.textContent = geojsonData.error;
alertBox.classList.remove("d-none");
return;
}
alertBox.classList.add("d-none"); // Cache l'alerte si tout va bien
if (map.getSource('locations')) {
map.getSource('locations').setData(geojsonData);
}
loadDataIntoLocationsTable(geojsonData.features);
})
.catch(error => {
alertBox.textContent = gettext("Erreur lors de la récupération des données.");
alertBox.classList.remove("d-none");
console.error("Erreur :", error);
});
}
function updateAssets() {
const selectedLocationsIds = Array.from(selected_locations_set);
const alertBox = document.getElementById("asset-alert");
const form = document.getElementById('assets-filter-form');
const formData = new FormData(form);
// Update URL
updateURLFromForms('filter-form', 'assets-filter-form');
// On convertit FormData en objet
const filters = {};
for (const [key, value] of formData.entries()) {
if (value.trim() !== '') {
// Si le champ existe déjà (checkbox, multiple select), on stocke en tableau
if (filters[key]) {
if (!Array.isArray(filters[key])) {
filters[key] = [filters[key]];
}
filters[key].push(value);
} else {
filters[key] = value;
}
}
}
try {
parsedGeom = JSON.parse(filter_geom);
} catch (e) {
parsedGeom = null;
}
fetch(assetsgeojsonUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken') // si CSRF est activé
},
body: JSON.stringify({
locations_ids: selectedLocationsIds,
filter_geom: parsedGeom,
q: (document.getElementById('assets-search-input') ? document.getElementById('assets-search-input').value.trim() : ''),
search_mode: (document.querySelector('input[name="assets-search-mode"]:checked') ? document.querySelector('input[name="assets-search-mode"]:checked').value : 'and'),
...filters // Ajout de tous les champs de filtre
})
})
.then(response => response.json())
.then(geojsonData => {
if (geojsonData.error) {
// Affiche l'erreur dans le div Bootstrap
alertBox.textContent = geojsonData.error;
alertBox.classList.remove("d-none");
return;
}
alertBox.classList.add("d-none"); // Cache l'alerte si tout va bien
if (map.getSource('assets')) {
map.getSource('assets').setData(geojsonData);
}
loadDataIntoAssetsTable(geojsonData.features);
})
.catch(error => {
alertBox.textContent = gettext("Erreur lors de la récupération des données.");
alertBox.classList.remove("d-none");
console.error("Erreur :", error);
});
}
function insertNoAssetsRowMessage() {
const $table = $('#assets-table');
const $tbody = $table.find('tbody');
// Supprime les messages précédents si existants
$tbody.find('tr.no-assets-message').remove();
$tbody.find('tr.no-records-found').remove();
if ($tbody.find('tr').length === 0) {
const colspan = $table.find('thead th').length || 1;
// Détecter si une recherche ou un filtre est actif
const searchVal = (document.getElementById('assets-search-input')?.value || '').trim();
const hasSelectedLocations = selected_locations_set.size > 0;
const assetFiltersForm = document.getElementById('assets-filter-form');
let hasAssetFilters = false;
if (assetFiltersForm) {
const formData = new FormData(assetFiltersForm);
for (const [, value] of formData.entries()) {
if (value.trim() !== '') { hasAssetFilters = true; break; }
}
}
let noResultsText;
if (!searchVal && !hasSelectedLocations && !hasAssetFilters) {
noResultsText = gettext('Sélectionnez des localisations ou lancez une recherche pour afficher les assets.');
} else {
noResultsText = gettext('Aucun asset ne correspond aux critères de recherche.');
}
const $msgRow = $(`
<tr class="no-assets-message">
<td colspan="${colspan}" class="text-center text-muted">
${noResultsText}
</td>
</tr>
`);
$tbody.append($msgRow);
}
}
// Hook appelé après chaque mise à jour de la table
$('#assets-table').on('post-body.bs.table', function () {
insertNoAssetsRowMessage();
});
map.on('draw.create', e => updateSelection(e.features[0]));
map.on('draw.update', e => updateSelection(e.features[0]));
map.on('draw.delete', e => {
resetMapFilter();
});
function updateSelection(geometry) {
filter_geom = JSON.stringify(geometry);
updateLocations();
updateAssets();
}
// Réinitialiser le filtre carte
function resetMapFilter() {
filter_geom = '';
updateLocations();
updateAssets();
}
function updateURLFromForms(...formIds) {
const params = new URLSearchParams();
formIds.forEach(formId => {
const form = document.getElementById(formId);
if (!form) return;
const formData = new FormData(form);
for (const [key, value] of formData.entries()) {
if (value.trim() !== '') {
params.append(key, value); // conserve les champs multiples
}
}
});
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.pushState({}, '', newUrl);
}
// --- Layout Management ---
const col_table = document.getElementById("col-table");
const col_map = document.getElementById("col-map");
const layoutBtnTable = document.getElementById("layout-btn-table");
const layoutBtnBoth = document.getElementById("layout-btn-both");
const layoutBtnMap = document.getElementById("layout-btn-map");
// Current layout state
let currentAssetLayout = typeof INITIAL_ASSET_LAYOUT !== 'undefined' ? INITIAL_ASSET_LAYOUT : 'both';
// Function to save layout preference to backend
function saveAssetLayoutPreference(layout) {
if (typeof saveAssetLayoutUrl === 'undefined') return;
fetch(saveAssetLayoutUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ layout: layout })
}).catch(err => console.warn('Could not save asset layout preference:', err));
}
// Function to apply layout
function applyAssetLayout(layout, savePreference = true) {
currentAssetLayout = layout;
// Reset classes
col_table.classList.remove("col-11", "col-12", "col-md-6", "order-2", "d-none");
col_map.classList.remove("col-11", "col-12", "col-md-6", "order-2", "d-none");
// Reset button active states
layoutBtnTable.classList.remove("text-primary");
layoutBtnBoth.classList.remove("text-primary");
layoutBtnMap.classList.remove("text-primary");
switch (layout) {
case 'table_only':
col_table.classList.add("col-12");
col_map.classList.add("d-none");
layoutBtnTable.classList.add("text-primary");
break;
case 'map_only':
col_map.classList.add("col-12");
col_table.classList.add("d-none");
layoutBtnMap.classList.add("text-primary");
break;
case 'both':
default:
col_table.classList.add("col-md-6");
col_map.classList.add("col-md-6");
layoutBtnBoth.classList.add("text-primary");
break;
}
// Refresh tables
$('#locations-table').bootstrapTable('resetView');
$('#assets-table').bootstrapTable('resetView');
// Resize map if visible
if (layout !== 'table_only' && typeof map !== 'undefined') {
setTimeout(() => map.resize(), 100);
}
// Save preference
if (savePreference) {
saveAssetLayoutPreference(layout);
}
}
// Layout button event handlers
layoutBtnTable.addEventListener("click", function() {
applyAssetLayout('table_only');
});
layoutBtnBoth.addEventListener("click", function() {
applyAssetLayout('both');
});
layoutBtnMap.addEventListener("click", function() {
applyAssetLayout('map_only');
});
// Initialize map resize on page load if visible
document.addEventListener('DOMContentLoaded', function() {
if (currentAssetLayout !== 'table_only' && typeof map !== 'undefined') {
setTimeout(() => map.resize(), 50);
}
});
// Totaux //
// ------ //
const btnTotals = document.getElementById('totals_button');
const overlay = document.getElementById('totalsOverlay');
const closeBtn = document.getElementById('totalsClose');
const contentEl = document.getElementById('totalsContent');
function getAssetsFromSource() {
const $table = $('#assets-table');
if (!$table.length || typeof $table.bootstrapTable !== 'function') return [];
// Récupère toutes les données en mémoire (toutes pages, filtrées côté client)
const all = $table.bootstrapTable('getData', {
useCurrentPage: false,
includeHiddenRows: false
}) || [];
// Si la sélection custom contient des clés -> on filtre dessus
if (window.selected_assets_set && selected_assets_set.size > 0) {
const out = [];
for (const row of all) {
// on reconstruit la clé exactement comme pour l'ensemble
const key = assetKey(row.asset_type, row.id);
if (selected_assets_set.has(String(key))) out.push(row);
}
// Si rien ne matche (ex: décalage de filtre), on retombe sur "all"
return out.length ? out : all;
}
// Sinon: pas de sélection => on prend tout ce qui est filtré/chargé
return all;
}
// Regroupe par catégorie, puis par modèle
function groupByCategoryAndModel(assets) {
const byCat = {};
for (const a of assets) {
const cat = a.category || gettext('Sans catégorie');
const model = a.model || '—';
byCat[cat] ??= {};
byCat[cat][model] ??= { count: 0, length_m: 0, area_m2: 0 };
byCat[cat][model].count += 1;
byCat[cat][model].length_m += Number(a.length) || 0;
byCat[cat][model].area_m2 += Number(a.area) || 0;
}
return byCat;
}
function fmtInt(n){ return (Math.round(n)||'-').toLocaleString(); }
function renderTotals() {
const assets = getAssetsFromSource();
const grouped = groupByCategoryAndModel(assets);
const isCustomSelection = window.selected_assets_set && selected_assets_set.size > 0;
// En-tête récap global
const headerCount = interpolate(
ngettext('%(count)s élément pris en compte',
'%(count)s éléments pris en compte',
assets.length),
{ count: assets.length },
true
);
const selectionSuffix = isCustomSelection
? gettext('(sélection)')
: gettext('(table)');
let html = `
<div class="mb-3 text-muted">
${headerCount} ${selectionSuffix}
</div>
`;
// Pour chaque catégorie: un tableau
const categories = Object.keys(grouped).sort((a,b)=>a.localeCompare(b, 'fr'));
if (!categories.length) {
contentEl.innerHTML = `<div class="text-center text-muted">${gettext('Aucune donnée disponible')}</div>`;
return;
}
for (const cat of categories) {
const models = grouped[cat];
const modelNames = Object.keys(models).sort((a,b)=>a.localeCompare(b, 'fr'));
// Lignes du body (par modèle)
const bodyRows = modelNames.map(m => {
const g = models[m];
return `
<tr>
<td>${m}</td>
<td class="text-end">${fmtInt(g.count)}</td>
<td class="text-end">${fmtInt(g.length_m)}</td>
<td class="text-end">${fmtInt(g.area_m2)}</td>
</tr>
`;
}).join('');
// Totaux par catégorie
const catTotals = modelNames.reduce((acc, m) => {
acc.count += models[m].count;
acc.length_m += models[m].length_m;
acc.area_m2 += models[m].area_m2;
return acc;
}, {count:0, length_m:0, area_m2:0});
html += `
<div class="mb-3 pb-2 border-bottom">
<h6 class="mb-2 d-flex align-items-center gap-2">
<span class="badge text-bg-secondary">${cat}</span>
<small class="text-muted">(${fmtInt(catTotals.count)} ${gettext('au total')})</small>
</h6>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>${gettext('Modèle')}</th>
<th class="text-end">${gettext('Nombre')}</th>
<th class="text-end">${gettext('Longueur (m)')}</th>
<th class="text-end">${gettext('Surface (m²)')}</th>
</tr>
</thead>
<tbody>${bodyRows || `
<tr><td colspan="4" class="text-center text-muted">${gettext('Aucun modèle')}</td></tr>
`}</tbody>
<tfoot>
<tr class="fw-semibold">
<td>${gettext('Total catégorie')}</td>
<td class="text-end">${fmtInt(catTotals.count)}</td>
<td class="text-end">${fmtInt(catTotals.length_m)}</td>
<td class="text-end">${fmtInt(catTotals.area_m2)}</td>
</tr>
</tfoot>
</table>
</div>
</div>
`;
}
contentEl.innerHTML = html;
}
// Ouvrir
btnTotals.addEventListener('click', () => {
renderTotals();
overlay.classList.add('show');
// Accessibilité: focus sur le bouton "Fermer"
setTimeout(() => closeBtn.focus(), 0);
});
// Fermer
function hideOverlay() { overlay.classList.remove('show'); }
closeBtn.addEventListener('click', hideOverlay);
// ESC pour fermer
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && overlay.classList.contains('show')) hideOverlay();
});
// Option: cliquer en dehors du contenu pour fermer (si tu ajoutes un wrapper)
// overlay.addEventListener('click', (e) => {
// if (e.target === overlay) hideOverlay();
// });
// ---------------------
// Widget de recherche assets
// ---------------------
document.addEventListener('DOMContentLoaded', function () {
const searchInput = document.getElementById('assets-search-input');
const searchBtn = document.getElementById('assets-search-btn');
const searchClear = document.getElementById('assets-search-clear');
if (!searchInput) return;
function runAssetsSearch() {
const hasVal = searchInput.value.trim() !== '';
searchClear.style.visibility = hasVal ? 'visible' : 'hidden';
updateLocations();
updateAssets();
}
searchBtn.addEventListener('click', runAssetsSearch);
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { e.preventDefault(); runAssetsSearch(); }
else if (e.key === 'Escape') { searchInput.value = ''; runAssetsSearch(); }
});
searchClear.addEventListener('click', function () {
searchInput.value = '';
searchClear.style.visibility = 'hidden';
updateLocations();
updateAssets();
});
// Reset filters also clears search
const resetLocBtn = document.getElementById('reset-filters');
const resetAssBtn = document.getElementById('reset-assets-filters');
if (resetLocBtn) {
resetLocBtn.addEventListener('click', function () {
searchInput.value = '';
searchClear.style.visibility = 'hidden';
});
}
if (resetAssBtn) {
resetAssBtn.addEventListener('click', function () {
searchInput.value = '';
searchClear.style.visibility = 'hidden';
});
}
});
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');