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

2661 lines
104 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 interventions_geojson = {};
let visibleRows = [];
let allInterventions = []; // full dataset
let currentMapFilteredData = []; // result of map-based filtering
let selectedLocations = [];
// items: { ct, obj, type, type_label, thematic, name, code, label, lon, lat }
const INTERVENTION_LIST_CONTEXT_KEY = 'interventions:list_context';
const interventionsgeojsonloader = new GeojsonLoader();
const interventions_layer_defs = [
{
'id': 'interventions',
'type': 'circle',
'source': 'interventions',
//filter: ['has', 'point_count'],
minzoom: 0,
maxzoom: 24,
'paint': {
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
10, 2,
22, 15
],
//'circle-color': '#0094ff',
'circle-color': INTERVENTION_STATUS_COLOR_EXPR,
'circle-opacity': 0.7
}
}
// {
// id: 'interventions-cluster-count',
// type: 'symbol',
// source: 'interventions',
// //filter: ['has', 'point_count'],
// minzoom: 0,
// maxzoom: 24,
// layout: {
// 'text-field': [
// 'case',
// ['has', 'point_count_abbreviated'], // Si la propriété 'point_count_abbreviated' existe
// ['get', 'point_count_abbreviated'], // Afficher la valeur de 'point_count_abbreviated'
// '1' // Sinon, afficher '1'
// ],
// 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
// 'text-size': 12
// }
// }
]
handleGeojsonMapLoading(init=true, interventionsgeojsonloader, 'interventions', interventions_layer_defs, cluster=false);
// Load Map
maploader.loadMap("map");
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":.1}},{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();
});
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);
}
});
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();
}
});
map.on('click', 'interventions', function(e) {
const radiusInPixels = 10; // Rayon autour du clic
const features = map.queryRenderedFeatures([
[e.point.x - radiusInPixels, e.point.y - radiusInPixels],
[e.point.x + radiusInPixels, e.point.y + radiusInPixels]
], {
layers: ['interventions']
});
if (!features.length) {
return;
}
// Générer le contenu de la popup avec tous les points proches
const popupContent = `
<div class="popup-scrollable">
${features.map((feature) => {
const props = feature.properties;
// Format the begin_time date if present
let beginDateStr = '-';
if (props.begin_time) {
const beginDate = new Date(props.begin_time);
beginDateStr = beginDate.toLocaleDateString('fr-BE', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
return `
<div>
<strong><a href="${interventionsurl}${props.id}/">${props.code || gettext('Intervention')}</a></strong><br/>
${gettext("Statut")}: ${statusFormatter(props.status, props)}<br/>
${gettext("Date de début")}: ${beginDateStr}<br/>
${gettext("Titre")}: ${props.title || '-'}
</div>
<hr/>
`;
}).join('')}
</div>
`;
// Afficher la popup à l’endroit du clic
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(popupContent)
.addTo(map);
});
map.on('mouseenter', 'interventions', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'interventions', () => {
map.getCanvas().style.cursor = '';
});
function persistInterventionsListContext() {
const $table = $('#interventions-table');
if (!$table.length || typeof $table.bootstrapTable !== 'function') {
return;
}
let data;
try {
data = $table.bootstrapTable('getData');
} catch (error) {
console.warn('Unable to retrieve interventions data for navigation context.', error);
return;
}
if (!Array.isArray(data) || data.length === 0) {
sessionStorage.removeItem(INTERVENTION_LIST_CONTEXT_KEY);
return;
}
const items = data
.map(row => {
if (!row || row.id === undefined || row.id === null) {
return null;
}
return {
id: row.id,
code: row.code || '',
title: row.title || ''
};
})
.filter(Boolean);
if (!items.length) {
sessionStorage.removeItem(INTERVENTION_LIST_CONTEXT_KEY);
return;
}
const options = $table.bootstrapTable('getOptions') || {};
const context = {
items,
sortName: options.sortName || null,
sortOrder: options.sortOrder || null,
searchText: options.searchText || '',
listUrl: window.location.pathname + (window.location.search || ''),
timestamp: Date.now()
};
try {
sessionStorage.setItem(INTERVENTION_LIST_CONTEXT_KEY, JSON.stringify(context));
} catch (error) {
console.warn('Unable to persist interventions navigation context.', error);
}
}
// Load initial data
const params = new URLSearchParams(window.location.search).toString();
interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl+`?${params}`);
// Reset table if error occurs when loading GeoJSON
interventionsgeojsonloader.addEventListener("geojsonError", event => {
loadDataIntoInterventionsTable([]);
});
// Constante de limite
const MAX_ITEMS = 5000;
// Load data into table when GeoJSON is loaded
interventionsgeojsonloader.addEventListener("geojsonLoaded", event => {
var geojson_data = event.detail;
loadDataIntoInterventionsTable(geojson_data.features);
interventions_geojson = geojson_data;
allInterventions = geojson_data.features; // Store the full dataset for later use
currentMapFilteredData = [...allInterventions];
// Afficher/masquer l'alerte "limite atteinte"
const alertEl = document.getElementById('filter-limit-alert');
if (alertEl) {
const reached = (allInterventions?.length || 0) >= MAX_ITEMS;
alertEl.classList.toggle('d-none', !reached);
// (Optionnel) infobulle avec le nombre exact reçu
// if (reached) alertEl.title = `Réception: ${allInterventions.length} éléments (seuls 5 000 sont affichés)`;
}
});
// Apply filters
document.getElementById('apply-filters').addEventListener('click', async function(e) {
const beginDateMin = document.getElementById('begin_date_min').value;
const beginDateMax = document.getElementById('begin_date_max').value;
if (beginDateMin && beginDateMax && new Date(beginDateMin) > new Date(beginDateMax)) {
alert(gettext('La date min ne peut pas être après la date max.'));
return;
}
const endDateMin = document.getElementById('end_date_min').value;
const endDateMax = document.getElementById('end_date_max').value;
if (endDateMin && endDateMax && new Date(endDateMin) > new Date(endDateMax)) {
alert(gettext('La date min ne peut pas être après la date max.'));
return;
}
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() !== '') { // Check that the field is not empty
params.append(key, value);
}
}
// Save preferences before redirecting
await savePreferencesFromForm(params);
if (selectedLocations.length) {
const payload = selectedLocations.map(x => ({ ct: x.ct, obj: x.obj }));
params.set('locations', JSON.stringify(payload));
}
else {
params.delete('locations');
}
// Update URL
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.pushState({}, '', newUrl);
// Update filter badge after URL change
updateFilterBadge();
// Preserve active search if any
const _activeQ = $('#interventions-search-input').val().trim();
const _activeMode = document.querySelector('input[name="search-mode"]:checked')?.value || 'and';
if (_activeQ) {
params.set('q', _activeQ);
params.set('search_mode', _activeMode);
}
// Load filtered data
handleGeojsonMapLoading(init=false, interventionsgeojsonloader, 'interventions');
interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl+`?${params}`);
// Fermer la modale de filtre
const filterModal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
if (filterModal) {
filterModal.hide();
}
});
// Reset filters
document.getElementById('reset-filters').addEventListener('click', async function(e) {
const form = document.getElementById('filter-form');
form.reset(); // Reset form fields
// Uncheck all checkboxes
form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = false;
});
togglePauseReasonFilter();
filterCategoriesByThematics();
filterProvidersAndContracts();
// Explicitly clear date fields
document.getElementById('begin_date_min').value = '';
document.getElementById('begin_date_max').value = '';
document.getElementById('end_date_min').value = '';
document.getElementById('end_date_max').value = '';
// Save empty preferences
if (typeof savePreferencesUrl !== 'undefined') {
try {
await fetch(savePreferencesUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ filters: {}, sort: {} })
});
} catch (err) {
// Silently fail
}
}
// Remove URL parameters
window.history.pushState({}, '', window.location.pathname);
// Update filter badge after URL change
updateFilterBadge();
// Clear search input too
$('#interventions-search-input').val('');
$('#interventions-search-clear').css('visibility', 'hidden');
// Reload all data without filters or search
handleGeojsonMapLoading(init=false, interventionsgeojsonloader, 'interventions');
interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl);
// Fermer la modale de filtre
const filterModal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
if (filterModal) {
filterModal.hide();
}
});
// Filter tables by map bounds
$("#filterzone").on( "click", function(e) {
e.preventDefault();
filterTableByMapBounds(map, interventions_geojson, loadDataIntoInterventionsTable);
});
// Loads data into table
function loadDataIntoInterventionsTable(features) {
const tableData = features.map((feature) => {
return {
actions: '', // Column for action icons, manipulated by the formatter
id: feature.id,
code: feature.properties.code,
status: feature.properties.status,
prep_state: feature.properties.prep_state,
maintain_type: feature.properties.maintain_type,
title: feature.properties.title,
location_code: feature.properties.location_code || '',
begin_time: feature.properties.begin_time,
assigned_provider: feature.properties.assigned_provider || '',
contract_number: feature.properties.contract_number || '',
contract_provider: feature.properties.contract_provider || '',
origin_type: feature.properties.origin_type || '',
source_category: feature.properties.source_category || '',
lon: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[0] : null, // Longitude
lat: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[1] : null // Latitude
};
});
$('#interventions-spinner').hide();
$('#interventions-table-container').show();
$('#interventions-table').bootstrapTable('load', tableData);
}
// Refresh table view when switching tabs
$('#interventions-table-tab').on('click', function () {
$('#interventions-table').bootstrapTable('resetView');
});
// Add action buttons for each row of the table
function actionsFormatter(value, row, index) {
resp = '';
// 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 (selected_interventions_set.has(row.id)) {
icon_select = 'bi-check-square-fill';
} else {
icon_select = 'bi-square';
}
resp = resp +
`<button class="btn btn-sm btn-light intervention-btn-toggle" data-id="${row.id}" data-bs-toggle="tooltip" title="${gettext("sélection de l'intervention")}">
<i class="bi ${icon_select}"></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;
}
function codeFormatter(value, row) {
const maintenanceTypeColor = {
'corrective': 'danger',
'preventive': 'warning',
'ameliorative': 'success',
};
const maintenanceColorClass = maintenanceTypeColor[row.maintain_type] || 'text-muted';
return `<i class="bi bi-gear text-${maintenanceColorClass} small-icon me-1"></i><a href="${interventionsurl}${row.id}/">${value}</a>`;
}
function statusFormatter(value, row) {
const icons = {
'in_preparation': { label: gettext('En prépa'), icon: 'bi-pencil', color: 'secondary' },
'to_be_approved': { label: gettext('À approuver'), icon: 'bi-hourglass-split', color: 'warning' },
'to_be_planned': { label: gettext('À planifier'), icon: 'bi-calendar-plus', color: 'warning' },
'to_be_processed': { label: gettext('À traiter'), icon: 'bi-tools', color: 'danger' },
'assigned': { label: gettext('Pris en charge'), icon: 'bi-person-check', color: 'warning' },
'in_progress': { label: gettext('Démarré'), icon: 'bi-play-circle', color: 'primary' },
'on_pause': { label: gettext('En pause'), icon: 'bi-pause-circle', color: 'warning' },
'finished': { label: gettext('Terminé'), icon: 'bi-check-circle', color: 'info' },
'processed': { label: gettext('Traité'), icon: 'bi-check2-all', color: 'info' },
'to_be_corrected': { label: gettext('À corriger'), icon: 'bi-exclamation-circle', color: 'danger' },
'corrected': { label: gettext('Corrigé'), icon: 'bi-check-circle', color: 'info' },
'validated': { label: gettext('Validé'), icon: 'bi-award', color: 'success' },
'invoiced': { label: gettext('Comptabilisé'), icon: 'bi-receipt', color: 'dark' },
'closed': { label: gettext('Finalisé'), icon: 'bi-archive', color: 'dark' },
// 'archived': { label: gettext('Archivé'), icon: 'bi-box-seam', color: 'dark' },
'canceled': { label: gettext('Annulé'), icon: 'bi-x-circle', color: 'secondary' },
};
const prepStateIcons = {
'processed': { label: gettext('Prêt'), icon: 'bi-box', color: 'success' },
'not_processed': { label: gettext('En attente'), icon: 'bi-box', color: 'danger' },
};
const entry = icons[value];
if (!entry) return value;
let html = `<span class="badge text-bg-${entry.color} status-${value}"><i class="bi ${entry.icon} me-2"></i>${entry.label}</span>`;
// Add preparation state indicator if present
if (row && row.prep_state && row.prep_state !== 'none' && prepStateIcons[row.prep_state]) {
const prepEntry = prepStateIcons[row.prep_state];
html += `&nbsp;<span class="badge text-bg-${prepEntry.color}" title="${prepEntry.label}"><i class="bi ${prepEntry.icon}"></i></span>`;
}
return html;
}
function titleFormatter(value, row) {
// Escape HTML to prevent XSS
const escapedTitle = $('<div/>').text(value).html();
// Create a cell with the title, limited to 2 lines with tooltip showing full text only if truncated
// We store the original text in data-original-title for later use
return `<span class="intervention-title-cell" data-truncated="false" data-original-title="${escapedTitle}">${escapedTitle}</span>`;
}
// Function to detect and mark truncated text in title cells
function detectTruncatedTitles() {
$('.intervention-title-cell').each(function() {
const element = this;
// Check if the element's scrollHeight (actual content height) exceeds clientHeight (visible height)
const isTruncated = element.scrollHeight > element.clientHeight;
$(element).attr('data-truncated', isTruncated ? 'true' : 'false');
// Only add title attribute if text is truncated
if (isTruncated) {
const originalTitle = $(element).attr('data-original-title');
$(element).attr('title', originalTitle);
} else {
// Remove title attribute if text is not truncated
$(element).removeAttr('title');
}
});
}
function contractFormatter(value, row) {
// Format: "E21.081 (prestataire)"
if (!value && !row.contract_provider) {
return '';
}
const contractNumber = value || '';
const providerName = row.contract_provider || '';
if (contractNumber && providerName) {
return `${contractNumber} (${providerName})`;
} else if (contractNumber) {
return contractNumber;
} else if (providerName) {
return providerName;
}
return '';
}
// ### Actions on the map ### //
// -----------------------------//
map.on('click', 'interventions-clusters', async (e) => {
const currentZoom = map.getZoom();
const features = map.queryRenderedFeatures(e.point, {
layers: ['interventions-clusters']
});
const center = features[0].geometry.coordinates;
if (currentZoom > 15) {
const { lng, lat } = e.lngLat; // Clicked coordinates
const distance = 10; // Distance in meters for each side
// Convert distance to geographic degrees
const latOffset = distance / 111320; // 1° latitude = ~111,320 m
const lngOffset = distance / (111320 * Math.cos((lat * Math.PI) / 180));
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);
// Filter table data based on area
filterTableByMapBounds(map, interventions_geojson, loadDataIntoInterventionsTable);
} else {
const zoom = currentZoom + 2
map.easeTo({
center: center,
zoom
});
}
});
// Mouse pointer on clusters
map.on('mouseenter', 'interventions-clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'interventions-clusters', () => {
map.getCanvas().style.cursor = '';
});
// Initialization after page load
$( document ).ready(function() {
// Prepare initial sort from saved preferences
const initialSort = {};
if (typeof SAVED_SORT !== 'undefined' && SAVED_SORT.sortName) {
initialSort.sortName = SAVED_SORT.sortName;
initialSort.sortOrder = SAVED_SORT.sortOrder || 'asc';
}
$('#interventions-table').bootstrapTable({
columns: [
{ field: 'actions', title: '', formatter:'actionsFormatter', class: 'text-nowrap', searchable: false },
{ field: 'code', title: gettext('Code'), formatter: 'codeFormatter', class: 'text-nowrap', sortable: true },
{ field: 'status', title: gettext('Statut'), formatter: 'statusFormatter', class: 'text-nowrap', sortable: true },
{ field: 'title', title: gettext('Titre'), formatter: 'titleFormatter', sortable: true },
{ field: 'location_code', title: gettext('Localisation'), class: 'text-nowrap', sortable: true },
{ field: 'begin_time', title: gettext('Début'), formatter: 'dateFormatter', class: 'text-nowrap', sortable: true },
{ field: 'contract_number', title: gettext('Contrat'), formatter: 'contractFormatter', class: 'text-nowrap', visible: false, sortable: true },
{ field: 'origin_type', title: gettext("Type d'origine"), class: 'text-nowrap', visible: false, sortable: true },
{ field: 'source_category', title: gettext("Source"), class: 'text-nowrap', visible: false, sortable: true },
],
height: calculateTableHeight(), // Apply the calculated height
...initialSort // Spread saved sort if available
});
// Restore saved column visibility
_restoreColumnVisibility($('#interventions-table'));
$('#interventions-table').on('post-body.bs.table', function() {
// Persist context and mark truncated titles
persistInterventionsListContext();
detectTruncatedTitles();
// Recalculate widths/height without triggering another post-body loop
const $table = $('#interventions-table');
if ($table.data('bootstrap.table')) {
$table.bootstrapTable('resetView', { height: calculateTableHeight() });
}
});
// Register actions to the table
registerDetailAction('#interventions-table', '.detail-btn', interventionsurl);
registerZoomAction('#interventions-table', '.zoom-btn', map, 17);
// Register intervention selection action to table interventions
registerSelectInterventionAction('#interventions-table', '.intervention-btn-toggle');
// Réappliquer la hauteur quand une colonne est (dé)masquée, et mémoriser la visibilité
$('#interventions-table').on('column-switch.bs.table', function () {
_saveColumnVisibility($('#interventions-table'));
// After toggling columns, recalc widths for proper alignment
setTimeout(function(){
$('#interventions-table').bootstrapTable('resetView');
}, 0);
});
});
function syncMapWithTable() {
if (!map.getLayer('interventions')) {
return;
}
// Si le tableau montre toutes les lignes => reset du filtre
if (visibleRows.length === allInterventions.length) {
map.setFilter('interventions', null);
return;
}
// Extrait les IDs à afficher
const idsToShow = visibleRows.map(row => row.id);
// Applique un filtre sur la couche MapLibre
if (idsToShow.length > 0) {
map.setFilter('interventions', ['in', ['get', 'id'], ['literal', idsToShow]]);
} else {
// Si aucune ligne visible, masquer tous les points
map.setFilter('interventions', ['==', ['get', 'id'], '__none__']);
}
}
function getSearchableVisibleFields() {
const columns = $('#interventions-table').bootstrapTable('getVisibleColumns');
return columns
.filter(col => col.searchable !== false)
.map(col => col.field)
.filter(Boolean);
}
function customSearch(data, text) {
const searchableFields = getSearchableVisibleFields();
const lowerText = text.toLowerCase();
const filtered = data.filter(row =>
searchableFields.some(field => {
const value = row[field];
return value != null && value.toString().toLowerCase().includes(lowerText);
})
);
visibleRows = filtered;
syncMapWithTable();
return filtered;
}
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) {
const source = map.getSource('interventions');
if (!source) return;
const data = source._data || source._geojson || source._options?.data;
if (!data || !data.features) return;
const selectedIds = data.features
.filter(f => f && f.geometry && f.geometry.type === "Point" && turf.booleanPointInPolygon(f, geometry))
.map(f => f.properties.id);
currentMapFilteredData = allInterventions.filter(row => selectedIds.includes(row.id));
// Recharge la table — recherche utilisateur sera réappliquée juste après
loadDataIntoInterventionsTable(currentMapFilteredData);
// Simule une recherche utilisateur en réinjectant le texte existant
const currentSearchText = $('#interventions-table').bootstrapTable('getOptions').searchText || '';
$('#interventions-table').bootstrapTable('refreshOptions', {
searchText: currentSearchText
});
}
// Réinitialiser le filtre carte
function resetMapFilter() {
// Recharge les données
loadDataIntoInterventionsTable(allInterventions);
// Simule une recherche utilisateur en réinjectant le texte existant
const currentSearchText = $('#interventions-table').bootstrapTable('getOptions').searchText || '';
$('#interventions-table').bootstrapTable('refreshOptions', {
searchText: currentSearchText
});
}
// Positionnne le filtre et le bouton d'ajout dans la toolbar du tableau
// document.addEventListener("DOMContentLoaded", function () {
// $('#interventions-table').on('post-body.bs.table', function () {
// const $toolbar = $('#interventions-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);
// // Apply the correct width to the dropdown-menu
// const parent_col = $('#col-left')[0];
// const parentWidth = parent_col.offsetWidth - 30;
// $('.filter-dropdown').width(`${parentWidth}px`);
// }
// });
// });
// Apply the correct width to the dropdown-menu
document.addEventListener("DOMContentLoaded", function () {
const parent_col = $('#col-left')[0];
const parentWidth = parent_col.offsetWidth - 40;
$('.filter-dropdown').width(`${parentWidth}px`);
});
function registerSelectInterventionAction(tableId, buttonClass) {
$(tableId).on('click', buttonClass, function () {
const $button = $(this);
const interventionId = $(this).data('id'); // Retrieve the ID from the button's data attribute
if (interventionId) {
toggleSelectedIntervention(interventionId, $button);
}
});
}
function toggleSelectedIntervention(interventionId, $button) {
if (selected_interventions_set.has(interventionId)) {
selected_interventions_set.delete(interventionId);
$button
.find('i').removeClass('bi-check-square-fill').addClass('bi-square');
} else {
selected_interventions_set.add(interventionId);
$button
.find('i').removeClass('bi-square').addClass('bi-check-square-fill');
}
updateInputAndStatus();
}
function updateInputAndStatus() {
const statusBar = document.getElementById("selection-status");
const ids = Array.from(selected_interventions_set);
const count = ids.length;
const msg = ngettext(
"%(count)s intervention sélectionnée",
"%(count)s interventions sélectionnées",
count
);
statusBar.innerText = interpolate(msg, { count }, true);
}
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("select-all").addEventListener("click", () => {
const allRows = $('#interventions-table').bootstrapTable('getData');
allRows.forEach(row => {
selected_interventions_set.add(parseInt(row.id));
const btn = document.querySelector(`.intervention-btn-toggle[data-id="${row.id}"]`);
if (btn) {
$(btn).find('i').removeClass('bi-square').addClass('bi-check-square-fill');
}
});
updateInputAndStatus();
});
document.getElementById("deselect-all").addEventListener("click", () => {
selected_interventions_set.clear();
const allRows = $('#interventions-table').bootstrapTable('getData');
allRows.forEach(row => {
const btn = document.querySelector(`.intervention-btn-toggle[data-id="${row.id}"]`);
if (btn) {
$(btn).find('i').removeClass('bi-check-square-fill').addClass('bi-square');
}
});
updateInputAndStatus();
});
// Initial state
updateInputAndStatus();
});
// Gestion des actions
document.addEventListener("DOMContentLoaded", function () {
const statusModal = new bootstrap.Modal(document.getElementById('statusChangeModal'));
const statusConfirmBtn = document.getElementById('confirm-status-change');
const statusErrorMsg = document.getElementById('status-error-message');
// Modal: associate to order
const associateOrderModalEl = document.getElementById('associateOrderModal');
const associateOrderModal = associateOrderModalEl ? new bootstrap.Modal(associateOrderModalEl) : null;
const orderSearchInput = document.getElementById('order-search');
const orderIdInput = document.getElementById('order-id');
const associateOrderConfirmBtn = document.getElementById('confirm-associate-order');
const associateOrderErrorMsg = document.getElementById('associate-order-error');
// Setup autocomplete for order search
if (orderSearchInput) {
$(orderSearchInput).autocomplete({
source: function(request, response) {
const url = new URL(availableOrdersUrl, window.location.origin);
url.searchParams.set('q', request.term);
fetch(url)
.then(res => res.json())
.then(data => {
const orders = (data.orders || []).map(o => ({
label: `${o.order_code} (${o.contract_number}) - ${o.status_display}`,
value: o.order_code,
id: o.id
}));
response(orders);
})
.catch(() => response([]));
},
minLength: 0,
select: function(event, ui) {
orderIdInput.value = ui.item.id;
return true;
},
change: function(event, ui) {
if (!ui.item) {
// Si l'utilisateur efface ou entre un texte invalide
orderIdInput.value = '';
orderSearchInput.value = '';
}
}
});
// Afficher toutes les commandes au focus
$(orderSearchInput).on('focus', function() {
$(this).autocomplete('search', '');
});
}
// Gérer le clic sur les éléments du dropdown
document.querySelectorAll('#bulk-action-dropdown .dropdown-item').forEach(item => {
item.addEventListener('click', function (e) {
e.preventDefault();
const action = this.dataset.action;
if (action === 'status-change') {
// Nettoyer l'alerte d'erreur précédente
statusErrorMsg.classList.add('d-none');
statusErrorMsg.textContent = '';
document.getElementById('new-status').value = '';
// Masquer le champ de raison d'annulation par défaut
const cancellationReasonGroup = document.getElementById('cancellation-reason-group');
if (cancellationReasonGroup) {
cancellationReasonGroup.classList.add('d-none');
}
// Info si aucune sélection
const statusSelInfo = document.getElementById('status-change-selection-info');
if (statusSelInfo) {
const hasSelection = (selected_interventions_set && selected_interventions_set.size > 0);
statusSelInfo.classList.toggle('d-none', hasSelection);
}
statusModal.show();
}
if (action === 'export') {
// L'ouverture de la modale est gérée via data-bs-target
const exportModalEl = document.getElementById('exportModal');
if (exportModalEl) {
// Info si aucune sélection
const exportSelInfo = document.getElementById('export-selection-info');
if (exportSelInfo) {
const hasSelection = (selected_interventions_set && selected_interventions_set.size > 0);
exportSelInfo.classList.toggle('d-none', hasSelection);
}
const instance = bootstrap.Modal.getOrCreateInstance(exportModalEl);
instance.show();
}
}
if (action === 'associate-order') {
if (associateOrderModal) {
associateOrderErrorMsg?.classList.add('d-none');
if (orderSearchInput) orderSearchInput.value = '';
if (orderIdInput) orderIdInput.value = '';
// Info si aucune sélection
const assocSelInfo = document.getElementById('associate-order-selection-info');
if (assocSelInfo) {
const hasSelection = (selected_interventions_set && selected_interventions_set.size > 0);
assocSelInfo.classList.toggle('d-none', hasSelection);
}
associateOrderModal.show();
}
}
if (action === 'export-sheets-zip') {
const ids = Array.from(selected_interventions_set || []);
if (!ids.length) {
showToast(gettext("Aucune intervention sélectionnée."), "danger");
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = exportSheetsZipUrl;
form.style.display = 'none';
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = getCookie('csrftoken');
form.appendChild(csrfInput);
const idsInput = document.createElement('input');
idsInput.type = 'hidden';
idsInput.name = 'ids';
idsInput.value = JSON.stringify(ids);
form.appendChild(idsInput);
document.body.appendChild(form);
form.submit();
setTimeout(() => document.body.removeChild(form), 1000);
}
if (action === 'execution-report') {
const ids = Array.from(selected_interventions_set || []);
const selectionInfo = document.getElementById('execution-report-selection-info');
const tooManyAlert = document.getElementById('execution-report-too-many');
const confirmBtn = document.getElementById('confirm-execution-report-btn');
// Reset
if (selectionInfo) selectionInfo.classList.add('d-none');
if (tooManyAlert) tooManyAlert.classList.add('d-none');
if (!ids.length) {
// Aucune sélection → informer que toutes les interventions filtrées seront incluses
if (selectionInfo) selectionInfo.classList.remove('d-none');
}
// Le contrôle de la limite (50 interventions) est effectué côté serveur,
// après application du filtre de dates — le bouton reste donc toujours actif ici.
if (confirmBtn) confirmBtn.removeAttribute('disabled');
}
});
});
// Gérer l'affichage du champ de raison d'annulation
const newStatusSelect = document.getElementById('new-status');
const cancellationReasonGroup = document.getElementById('cancellation-reason-group');
if (newStatusSelect && cancellationReasonGroup) {
newStatusSelect.addEventListener('change', function() {
if (this.value === 'canceled') {
cancellationReasonGroup.classList.remove('d-none');
} else {
cancellationReasonGroup.classList.add('d-none');
}
// Effacer les erreurs lors du changement
if (statusErrorMsg) {
statusErrorMsg.classList.add('d-none');
statusErrorMsg.textContent = '';
}
});
}
// Afficher l'astérisque obligatoire sur le commentaire quand le type 'Autre' est sélectionné (bulk)
const bulkCancellationType = document.getElementById('bulk-cancellation-type');
if (bulkCancellationType) {
bulkCancellationType.addEventListener('change', function () {
const reqMark = document.getElementById('bulkCancellationReasonRequired');
if (reqMark) reqMark.classList.toggle('d-none', this.value !== 'other');
});
}
// Export confirm
const exportConfirmBtn = document.getElementById('confirm-export-btn');
const exportError = document.getElementById('export-error');
if (exportConfirmBtn) {
exportConfirmBtn.addEventListener('click', () => {
const ids = Array.from(selected_interventions_set || []);
if (!ids.length) {
if (exportError) {
exportError.textContent = gettext("Veuillez sélectionner au moins une intervention.");
exportError.classList.remove('d-none');
}
showToast(gettext("Aucune intervention sélectionnée."), "danger");
return;
}
if (exportError) exportError.classList.add('d-none');
const fmt = (document.querySelector('input[name="export-format"]:checked')?.value || 'csv');
// Créer un formulaire caché pour POST (évite les URLs trop longues)
const form = document.createElement('form');
form.method = 'POST';
form.action = exportInterventionsUrl;
form.style.display = 'none';
// Ajouter le CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = getCookie('csrftoken');
form.appendChild(csrfInput);
// Ajouter le format
const formatInput = document.createElement('input');
formatInput.type = 'hidden';
formatInput.name = 'format';
formatInput.value = fmt;
form.appendChild(formatInput);
// Ajouter tous les IDs en un seul paramètre JSON (évite DATA_UPLOAD_MAX_NUMBER_FIELDS)
const idsInput = document.createElement('input');
idsInput.type = 'hidden';
idsInput.name = 'ids';
idsInput.value = JSON.stringify(ids);
form.appendChild(idsInput);
// Ajouter le formulaire au DOM et le soumettre
document.body.appendChild(form);
// Fermer la modale avant soumission
const exportModalEl = document.getElementById('exportModal');
if (exportModalEl) {
const instance = bootstrap.Modal.getOrCreateInstance(exportModalEl);
instance.hide();
}
form.submit();
// Nettoyer après un délai
setTimeout(() => {
document.body.removeChild(form);
}, 1000);
});
}
// Execution report – désactiver le bouton si trop d'interventions sélectionnées
const execConfirmBtn = document.getElementById('confirm-execution-report-btn');
const execReportTooManyAlert = document.getElementById('execution-report-too-many');
if (execConfirmBtn) {
execConfirmBtn.addEventListener('click', async function () {
const ids = Array.from(selected_interventions_set || []);
const reportTitle = (document.getElementById('execution-report-title')?.value || '').trim();
const dateFrom = (document.getElementById('execution-report-date-from')?.value || '').trim();
const dateTo = (document.getElementById('execution-report-date-to')?.value || '').trim();
// Fermer la modale et afficher l'overlay de progression
const modalEl = document.getElementById('executionReportModal');
if (modalEl) bootstrap.Modal.getOrCreateInstance(modalEl).hide();
const progressOverlay = document.getElementById('execution-report-progress-overlay');
const progressBar = document.getElementById('execution-report-progress-bar');
const progressLabel = document.getElementById('execution-report-progress-label');
const progressPct = document.getElementById('execution-report-progress-pct');
function setProgress(current, total) {
const pct = total > 0 ? Math.round((current / total) * 100) : 0;
if (progressBar) {
progressBar.style.width = pct + '%';
progressBar.setAttribute('aria-valuenow', pct);
}
if (progressLabel) progressLabel.textContent = total > 0
? interpolate(gettext('Intervention %(current)s sur %(total)s…'), {current, total}, true)
: gettext('Préparation…');
if (progressPct) progressPct.textContent = pct + '\u00a0%';
}
setProgress(0, 0);
if (progressOverlay) progressOverlay.style.display = 'flex';
execConfirmBtn.setAttribute('disabled', 'disabled');
const formData = new FormData();
formData.append('csrfmiddlewaretoken', getCookie('csrftoken'));
formData.append('ids', JSON.stringify(ids));
formData.append('filters', window.location.search);
formData.append('report_title', reportTitle);
if (dateFrom) formData.append('date_from', dateFrom);
if (dateTo) formData.append('date_to', dateTo);
let pollInterval = null;
function cleanup() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
if (progressOverlay) progressOverlay.style.display = 'none';
execConfirmBtn.removeAttribute('disabled');
}
try {
// 1) Lancer la génération en arrière-plan
const startResp = await fetch(executionReportStartUrl, { method: 'POST', body: formData });
const startData = await startResp.json();
if (!startResp.ok) {
throw new Error(startData.error || gettext('Erreur serveur'));
}
const { token, total } = startData;
setProgress(0, total);
// 2) Surveiller la progression par polling
await new Promise((resolve, reject) => {
pollInterval = setInterval(async () => {
try {
const pollUrl = executionReportPollBaseUrl.replace('__TOKEN__', token);
const pollResp = await fetch(pollUrl);
if (!pollResp.ok) { reject(new Error(gettext('Erreur de suivi'))); return; }
const job = await pollResp.json();
setProgress(job.current, job.total);
if (job.status === 'done') { clearInterval(pollInterval); pollInterval = null; resolve(); }
else if (job.status === 'error') { clearInterval(pollInterval); pollInterval = null; reject(new Error(job.error || gettext('Erreur lors de la génération'))); }
} catch (e) { reject(e); }
}, 600);
});
// 3) Télécharger le PDF
setProgress(total, total);
const a = document.createElement('a');
a.href = executionReportDownloadBaseUrl.replace('__TOKEN__', token);
a.download = 'rapport_execution_' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} catch (err) {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
showToast(err.message || gettext('Erreur lors de la génération du rapport.'), 'danger');
} finally {
cleanup();
}
});
}
// Confirmer le changement de statut dans la modale
statusConfirmBtn.addEventListener('click', function () {
const newStatus = document.getElementById('new-status').value;
if (!newStatus) {
statusErrorMsg.textContent = gettext("Veuillez sélectionner un statut.");
statusErrorMsg.classList.remove('d-none');
return;
}
// Vérifier si une raison d'annulation est requise
let cancellationReason = '';
let cancellationType = '';
if (newStatus === 'canceled') {
cancellationType = (document.getElementById('bulk-cancellation-type')?.value || '').trim();
if (!cancellationType) {
statusErrorMsg.textContent = gettext("Le type d'annulation est obligatoire.");
statusErrorMsg.classList.remove('d-none');
return;
}
cancellationReason = (document.getElementById('bulk-cancellation-reason')?.value || '').trim();
// Le commentaire est obligatoire uniquement pour le motif 'Autre'
if (cancellationType === 'other' && !cancellationReason) {
statusErrorMsg.textContent = gettext("Un commentaire est obligatoire pour le motif \"Autre\".");
statusErrorMsg.classList.remove('d-none');
return;
}
}
const selectedIds = Array.from(selected_interventions_set);
const payload = {
ids: selectedIds,
new_status: newStatus
};
if (newStatus === 'canceled') {
payload.cancellation_type = cancellationType;
payload.cancellation_reason = cancellationReason;
}
fetch('/interventions/bulk-change-status/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken') // Fonction ci-dessous
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
const invalidIds = data.invalid_ids || [];
const errorMessages = data.error_messages || {};
const validIds = selectedIds.filter(id => !invalidIds.includes(id));
// Afficher les messages d'erreur s'il y en a
if (invalidIds.length > 0) {
const errorContent = invalidIds.map(id => {
const msg = errorMessages[id] || gettext("Erreur inconnue");
return `<strong>${gettext("Intervention")} ${id}:</strong> ${msg}`;
}).join('<br>');
statusErrorMsg.innerHTML = errorContent;
statusErrorMsg.classList.remove('d-none');
showToast(`${invalidIds.length} ${gettext("intervention(s) ne peuvent pas passer au nouveau statut.")}`, "danger");
}
if (validIds.length > 0) {
const count = validIds.length;
const msg = ngettext(
"Le statut de %(count)s intervention a été modifié avec succès.",
"Le statut de %(count)s interventions a été modifié avec succès.",
count
);
showToast(interpolate(msg, { count }, true), "success");
// Mise à jour directe des lignes dans le tableau
validIds.forEach(id => {
$('#interventions-table').bootstrapTable('updateByUniqueId', {
id: id,
row: {
status: newStatus
}
});
});
}
// Check if any emails failed
if (data.email_failed_count && data.email_warning) {
showToast(data.email_warning, "warning");
}
})
.catch(error => {
console.error(error);
showToast(gettext("Une erreur est survenue."), "danger");
});
// Optionnel : recharger le tableau
$('#interventions-table').bootstrapTable('refresh');
statusModal.hide();
statusErrorMsg.classList.add('d-none');
// Réinitialiser les champs de raison d'annulation
const cancellationReasonField = document.getElementById('bulk-cancellation-reason');
if (cancellationReasonField) {
cancellationReasonField.value = '';
}
const cancellationTypeField = document.getElementById('bulk-cancellation-type');
if (cancellationTypeField) {
cancellationTypeField.value = '';
}
const bulkReqMark = document.getElementById('bulkCancellationReasonRequired');
if (bulkReqMark) bulkReqMark.classList.add('d-none');
});
// Confirmer l'association à une commande
if (associateOrderConfirmBtn) {
associateOrderConfirmBtn.addEventListener('click', async function () {
const orderId = orderIdInput ? orderIdInput.value : '';
if (!orderId) {
if (associateOrderErrorMsg) {
associateOrderErrorMsg.textContent = gettext("Veuillez sélectionner une commande.");
associateOrderErrorMsg.classList.remove('d-none');
}
return;
}
const selectedIds = Array.from(selected_interventions_set);
if (!selectedIds.length) {
showToast(gettext("Aucune intervention sélectionnée."), 'danger');
return;
}
try {
const res = await fetch(bulkAssociateOrderUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
order_id: parseInt(orderId),
ids: selectedIds,
})
});
const data = await res.json();
if (!res.ok) {
const msg = data && data.message ? data.message : gettext("Une erreur est survenue.");
showToast(msg, 'danger');
} else {
// success
showToast(data.message || gettext("Interventions associées."), 'success');
associateOrderModal?.hide();
// Optionnel: rafraîchir table pour refléter éventuelles colonnes liées
$('#interventions-table').bootstrapTable('refresh');
}
} catch (e) {
showToast(gettext("Une erreur est survenue."), 'danger');
}
});
}
});
function showToast(message, type = 'success') {
const toastEl = document.getElementById('app-toast');
const toastBody = document.getElementById('app-toast-body');
// Nettoyage : supprimer classes précédentes
toastEl.className = 'toast align-items-center text-white border-0';
// Appliquer la couleur (Bootstrap : success, danger, info, etc.)
toastEl.classList.add(`bg-${type}`);
// Injecter le message
toastBody.textContent = message;
// Afficher le toast
const toast = new bootstrap.Toast(toastEl);
toast.show();
}
// --- Layout Management ---
const col_left = document.getElementById("col-left");
const col_right = document.getElementById("col-right");
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 currentLayout = typeof INITIAL_LAYOUT !== 'undefined' ? INITIAL_LAYOUT : 'both';
// Function to calculate optimal table height
function calculateTableHeight() {
// Get the interventions-table-container element
const container = document.getElementById('interventions-table-container');
if (!container) return 500; // Fallback if container not found
// Start with col-left height (the entire column)
const colLeft = document.getElementById('col-left');
if (!colLeft) return 500;
// Get the nav tabs height
const navTabs = colLeft.querySelector('.nav.nav-tabs');
const navTabsHeight = navTabs ? navTabs.offsetHeight : 50;
// Get the toolbar (selection controls) height
const toolbar = document.querySelector('#interventions-table-content .d-flex.justify-content-between');
const toolbarHeight = toolbar ? toolbar.offsetHeight : 60;
// Get filter wrapper height
const filterWrapper = document.getElementById('filter-dropdown-wrapper');
const filterHeight = filterWrapper ? filterWrapper.offsetHeight : 32;
// Calculate: col-left height - nav tabs - toolbar - filter
const colHeight = colLeft.offsetHeight;
const availableHeight = colHeight - navTabsHeight - toolbarHeight - filterHeight;
return Math.max(availableHeight, 300); // Minimum 300px
}
// Helper to consistently apply the computed table height
function refreshTableHeight() {
const $table = $('#interventions-table');
// Guard: avoid calling before bootstrap-table is initialized
if (!$table.data('bootstrap.table')) return;
const tableHeight = calculateTableHeight();
$table.bootstrapTable('refreshOptions', { height: tableHeight });
$table.bootstrapTable('resetView');
}
// Function to save layout preference to backend
function saveLayoutPreference(layout) {
if (typeof saveLayoutUrl === 'undefined') return;
fetch(saveLayoutUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ layout: layout })
}).catch(err => console.warn('Could not save layout preference:', err));
}
// Function to apply layout
function applyLayout(layout, savePreference = true) {
currentLayout = layout;
// Reset classes
col_left.classList.remove("col-11", "col-12", "col-md-6", "order-2", "d-none");
col_right.classList.remove("col-11", "col-12", "col-md-6", "order-2", "d-none");
// Reset button text-primary states
layoutBtnTable.classList.remove("text-primary");
layoutBtnBoth.classList.remove("text-primary");
layoutBtnMap.classList.remove("text-primary");
switch (layout) {
case 'table_only':
// Show table full width, hide map completely
col_left.classList.add("col-12");
col_right.classList.add("d-none");
layoutBtnTable.classList.add("text-primary");
break;
case 'map_only':
// Show map full width, hide table completely
col_right.classList.add("col-12");
col_left.classList.add("d-none");
layoutBtnMap.classList.add("text-primary");
break;
case 'both':
default:
// Show both columns in 50/50 split
col_left.classList.add("col-md-6");
col_right.classList.add("col-md-6");
layoutBtnBoth.classList.add("text-primary");
break;
}
// Refresh table with dynamic height based on available space
refreshTableHeight();
// Resize map if visible
if (layout !== 'table_only' && typeof map !== 'undefined') {
setTimeout(() => {
map.resize();
}, 100);
}
// Save preference to backend
if (savePreference) {
saveLayoutPreference(layout);
}
}
// Initialize layout on page load
document.addEventListener('DOMContentLoaded', function() {
// Layout is already applied via server-side rendering
// Just refresh table height and map if needed
if (currentLayout !== 'map_only') {
refreshTableHeight();
}
if (currentLayout !== 'table_only' && typeof map !== 'undefined') {
setTimeout(() => map.resize(), 50);
}
});
// Layout button event handlers
layoutBtnTable.addEventListener("click", function() {
applyLayout('table_only');
});
layoutBtnBoth.addEventListener("click", function() {
applyLayout('both');
});
layoutBtnMap.addEventListener("click", function() {
applyLayout('map_only');
});
// Handle window resize for table height adjustment
let resizeTimeout;
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
refreshTableHeight();
}, 150);
});
// --- Locations Helpers ---
const $chipsLoc = document.getElementById('selected-locations-chips');
function thematicsFromSelect() {
const sel = document.getElementById('thematic');
return sel ? Array.from(sel.selectedOptions).map(o => o.value) : [];
}
function hasLocation(ct, obj) {
return selectedLocations.some(x => x.ct === ct && x.obj === obj);
}
function renderLocationChips() {
const wrap = document.getElementById('selected-locations-chips');
wrap.innerHTML = '';
// État vide → message léger
if (!selectedLocations.length) {
const empty = document.createElement('div');
empty.className = 'text-muted fst-italic';
empty.textContent = gettext('Aucune localisation sélectionnée');
wrap.appendChild(empty);
return;
}
// Liste Bootstrap
const ul = document.createElement('ul');
ul.id = 'selected-locations-list';
ul.className = 'list-group';
selectedLocations.forEach(item => {
const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center location-item';
// Bloc texte (label + sous-ligne)
const info = document.createElement('div');
info.className = 'd-flex flex-column flex-grow-1 me-3';
const title = document.createElement('span');
title.className = 'fw-medium text-truncate';
title.title = item.label || '';
title.textContent = item.label || '';
info.append(title);
// Badge thématique (à droite du label)
const badge = document.createElement('span');
badge.className = 'badge text-bg-secondary me-2 align-self-center';
badge.textContent = item.type_label || '';
// Bouton supprimer
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-sm btn-outline-secondary';
btn.setAttribute('aria-label', gettext('Retirer cette localisation'));
btn.setAttribute('data-action', 'remove-location');
btn.setAttribute('data-ct', item.ct);
btn.setAttribute('data-obj', item.obj);
btn.innerHTML = '<i class="bi bi-x"></i>';
li.append(info, badge, btn);
ul.appendChild(li);
});
wrap.appendChild(ul);
// Compteur (optionnel)
// const count = document.createElement('div');
// count.className = 'form-text mt-1';
// count.textContent = `${selectedLocations.length} localisation(s) sélectionnée(s)`;
// wrap.appendChild(count);
}
document.getElementById('selected-locations-chips')?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action="remove-location"]');
if (!btn) return;
const ct = parseInt(btn.getAttribute('data-ct'), 10);
const obj = parseInt(btn.getAttribute('data-obj'), 10);
selectedLocations = selectedLocations.filter(x => !(x.ct === ct && x.obj === obj));
renderLocationChips();
});
document.getElementById('clear-locations')?.addEventListener('click', () => { selectedLocations = []; renderLocationChips(); });
// Purger les chips hors thématiques quand l’utilisateur change:
function pruneLocationsBythematics(activethematics) {
if (!activethematics?.length) return;
selectedLocations = selectedLocations.filter(x => !x.thematic || activethematics.includes(x.thematic));
renderLocationChips();
}
(function initLocationsAutocomplete($){
const $input = $('#location-autocomplete');
// sécurité: simple échappement pour le rendu custom
function textNode(text) { return document.createTextNode(text ?? ''); }
$input.autocomplete({
minLength: 2,
delay: 180,
autoFocus: false,
appendTo: '#location-autocomplete-container',
source: async function(request, response) {
const thematics = thematicsFromSelect();
const params = new URLSearchParams({ q: request.term });
thematics.forEach(t => params.append('thematic', t));
try {
const res = await fetch(`${locationsAutocompleteUrl}?${params.toString()}`);
if (!res.ok) return response([]);
const data = await res.json();
// on masque les items déjà sélectionnés
const items = data
.filter(it => it?.value && !hasLocation(it.value.ct, it.value.obj))
.map(it => ({
label: it.label, // affiché dans la liste + focus
value: it.label, // jQuery UI attend une valeur; on ne l'utilise pas vraiment
ct: it.value.ct, obj: it.value.obj,
type: it.type, type_label: it.type_label, thematic: it.thematic,
name: it.name, code: it.code, lon: it.lon, lat: it.lat
}));
response(items);
} catch(e) {
response([]);
}
},
focus: function(event, ui) {
// NE PAS écrire dans l’input, on garde le texte tapé
event.preventDefault(); // équivaut à "return false" mais plus explicite
},
select: function(event, ui) {
// Ajoute un chip et vide le champ
selectedLocations.push({
ct: ui.item.ct, obj: ui.item.obj,
type: ui.item.type, type_label: ui.item.type_label, thematic: ui.item.thematic,
name: ui.item.name, code: ui.item.code, label: ui.item.label,
lon: ui.item.lon, lat: ui.item.lat
});
renderLocationChips();
// $(this).val('');
event.preventDefault(); // empêche jQuery UI d’insérer ui.item.value
}
})
// Rendu custom des lignes (badge de thématique)
.autocomplete('instance')._renderItem = function(ul, item) {
const li = $('<li>');
const row = $('<div class="d-flex justify-content-between align-items-center">');
const left = $('<span class="text-truncate">').append(textNode(item.label));
const right = $('<span class="badge text-bg-secondary ms-2">').append(textNode(item.type_label || ''));
row.append(left, right);
li.append(row).appendTo(ul);
return li;
};
// Fermer le menu si on clique ailleurs
$(document).on('click', (e) => {
if (!$.contains($('#location-autocomplete-container')[0], e.target)) {
$input.autocomplete('close');
}
});
// Quand les thématiques changent → purger chips hors scope + rafraîchir suggestions
$('#thematic').on('change', () => {
pruneLocationsBythematics(thematicsFromSelect());
$input.autocomplete('search', $input.val()); // relance une recherche sur le terme courant
});
})(jQuery);
document.addEventListener('DOMContentLoaded', function () {
if (Array.isArray(PRESELECTED_LOCATIONS) && PRESELECTED_LOCATIONS.length) {
// On injecte directement les objets prêts pour l’UI
selectedLocations = PRESELECTED_LOCATIONS;
renderLocationChips(); // rend la liste (ou tes chips si tu as gardé l’ancien rendu)
} else {
// Fallback: si jamais pas de variable injectée, on peut garder ton parsing d’URL
const urlSel = new URLSearchParams(window.location.search).get('locations');
if (urlSel) {
try {
const arr = JSON.parse(urlSel);
if (Array.isArray(arr)) {
// Minimal → optionnel, mais idéalement on ne passera plus par ici
selectedLocations = arr.map(x => ({
ct: x.ct, obj: x.obj,
type: '', type_label: '', thematic: '',
name: '', code: '',
label: `${gettext("Localisation")} (${x.ct}:${x.obj})`,
lon: null, lat: null
}));
renderLocationChips();
}
} catch(e) { /* ignore */ }
}
}
});
function setOptionIcons(select) {
for (const opt of select.options) {
// Mémoriser le libellé “pur” une fois
if (!opt.dataset.rawLabel) opt.dataset.rawLabel = opt.textContent.trim();
const icon = opt.selected ? '🗸 ' : ' '; // 🗴 ☐ ● •
opt.textContent = icon + opt.dataset.rawLabel;
}
}
function updateSummary(select) {
const summaryId = select.getAttribute('data-summary-target');
if (!summaryId) return;
const el = document.getElementById(summaryId);
if (!el) return;
const selected = Array.from(select.selectedOptions).map(o => o.dataset.rawLabel || o.textContent.trim());
if (selected.length === 0) {
el.textContent = gettext('Aucune sélection');
} else if (selected.length <= 3) {
el.textContent = `${selected.length} ${gettext("sélection(s)")} : ${selected.join(', ')}`;
} else {
el.textContent = `${selected.length} ${gettext("sélection(s)")}`;
}
}
function enhance(select) {
select.classList.add('bs-iconized', 'form-select');
// Rendu initial
setOptionIcons(select);
updateSummary(select);
// Mise à jour sur changement
select.addEventListener('change', function() {
setOptionIcons(select);
updateSummary(select);
});
}
function togglePauseReasonFilter() {
// Chercher les checkboxes de statut au lieu du select
const statusCheckboxes = document.querySelectorAll('input[name="status"]:checked');
const pauseReasonContainer = document.getElementById('pause-reason-filter');
const pauseReasonCheckboxes = document.querySelectorAll('input[name="pause_reason"]');
const pauseReasonButtons = pauseReasonContainer?.querySelectorAll('.filter-section-buttons button');
if (!pauseReasonContainer || pauseReasonCheckboxes.length === 0) {
return;
}
const selectedStatuses = Array.from(statusCheckboxes).map(checkbox => checkbox.value);
const onlyPauseSelected = selectedStatuses.length === 1 && selectedStatuses[0] === 'on_pause';
pauseReasonContainer.classList.toggle('d-none', !onlyPauseSelected);
// Disable/enable checkboxes and buttons
pauseReasonCheckboxes.forEach(checkbox => {
checkbox.disabled = !onlyPauseSelected;
});
pauseReasonButtons?.forEach(btn => {
btn.disabled = !onlyPauseSelected;
});
if (!onlyPauseSelected) {
pauseReasonCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
}
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('select[multiple][data-enhance="icons"]').forEach(enhance);
// Listen to status checkbox changes
document.querySelectorAll('input[name="status"]').forEach(checkbox => {
checkbox.addEventListener('change', togglePauseReasonFilter);
});
togglePauseReasonFilter();
// Listen to thematic checkbox changes to filter categories
document.querySelectorAll('input[name="thematic"]').forEach(checkbox => {
checkbox.addEventListener('change', () => {
filterCategoriesByThematics();
filterProvidersAndContracts();
});
});
// Initialize category filtering on page load
filterCategoriesByThematics();
filterProvidersAndContracts();
// Filter contracts when providers change
document.querySelectorAll('input[name="provider"]').forEach(checkbox => {
checkbox.addEventListener('change', filterProvidersAndContracts);
});
document.querySelectorAll('input[name="contract"]').forEach(checkbox => {
checkbox.addEventListener('change', filterProvidersAndContracts);
});
document.querySelectorAll('input[name="assigned_team"]').forEach(checkbox => {
checkbox.addEventListener('change', filterProvidersAndContracts);
});
// Handle select all / deselect all buttons for filter checkboxes
document.querySelectorAll('.select-all-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const field = this.getAttribute('data-field');
const container = document.querySelector(`.filter-options[data-field="${field}"]`);
if (container) {
const checkboxes = container.querySelectorAll('input[type="checkbox"]:not(:disabled)');
checkboxes.forEach(checkbox => {
// Only check visible checkboxes (for categories filtered by thematics)
const parent = checkbox.closest('.filter-option');
if (!parent || parent.style.display !== 'none') {
checkbox.checked = true;
}
});
// Trigger change event for status to update pause_reason filter
if (field === 'status') {
togglePauseReasonFilter();
}
// Trigger change event for thematic to update categories filter
if (field === 'thematic') {
filterCategoriesByThematics();
filterProvidersAndContracts();
}
if (field === 'provider') {
filterProvidersAndContracts();
}
if (field === 'contract') {
filterProvidersAndContracts();
}
if (field === 'assigned_team') {
filterProvidersAndContracts();
}
}
});
});
document.querySelectorAll('.deselect-all-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const field = this.getAttribute('data-field');
const container = document.querySelector(`.filter-options[data-field="${field}"]`);
if (container) {
const checkboxes = container.querySelectorAll('input[type="checkbox"]:not(:disabled)');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
// Trigger change event for status to update pause_reason filter
if (field === 'status') {
togglePauseReasonFilter();
}
// Trigger change event for thematic to update categories filter
if (field === 'thematic') {
filterCategoriesByThematics();
filterProvidersAndContracts();
}
if (field === 'provider') {
filterProvidersAndContracts();
}
if (field === 'contract') {
filterProvidersAndContracts();
}
if (field === 'assigned_team') {
filterProvidersAndContracts();
}
}
});
});
// Category parent-child show/hide
document.querySelectorAll('input[name="category"]').forEach(cb => {
cb.addEventListener('change', function() {
const option = this.closest('.filter-option');
const catId = option ? option.getAttribute('data-category-id') : null;
const parentId = option ? option.getAttribute('data-parent-id') : null;
if (catId && !parentId) {
// Parent toggled: show/hide (aucune) chip, children + auto-check on select
const container = document.querySelector('.filter-options[data-field="category"]');
if (container) {
const controls = container.querySelector(`.filter-subcat-controls[data-for-parent="${catId}"]`);
if (controls) controls.style.display = this.checked ? '' : 'none';
const noneDiv = container.querySelector(`.filter-option[data-category-id="${catId}_none"]`);
if (noneDiv) {
noneDiv.style.display = this.checked ? '' : 'none';
const noneCb = noneDiv.querySelector('input[type="checkbox"]');
if (noneCb) noneCb.checked = this.checked;
}
container.querySelectorAll(`.filter-option[data-parent-id="${catId}"]:not([data-category-id="${catId}_none"])`).forEach(childOption => {
if (this.checked) {
if (option.style.display !== 'none') {
childOption.style.display = '';
const childCb = childOption.querySelector('input[type="checkbox"]');
if (childCb) childCb.checked = true;
}
} else {
childOption.style.display = 'none';
const childCb = childOption.querySelector('input[type="checkbox"]');
if (childCb) childCb.checked = false;
}
});
}
}
});
});
// Create Tous/Aucun buttons + (aucune) chip + init visibility for sub-category groups
const catContainer = document.querySelector('.filter-options[data-field="category"]');
if (catContainer) {
const parentIds = new Set();
catContainer.querySelectorAll('.filter-option[data-parent-id]:not([data-parent-id=""])').forEach(opt => {
parentIds.add(opt.getAttribute('data-parent-id'));
});
const urlCategories = new URLSearchParams(window.location.search).getAll('category');
parentIds.forEach(parentId => {
const allChildren = Array.from(catContainer.querySelectorAll(`.filter-option[data-parent-id="${parentId}"]`));
if (!allChildren.length) return;
const parentOption = catContainer.querySelector(`.filter-option[data-category-id="${parentId}"]`);
const parentCb = parentOption ? parentOption.querySelector('input[type="checkbox"]') : null;
// Parent checkbox becomes a UI toggle only — don't submit its value
if (parentCb) parentCb.name = 'category_toggle';
// Add (aucune sous-catégorie) chip before first real child
const noneId = `category_none_${parentId}`;
const noneDiv = document.createElement('div');
noneDiv.className = 'filter-option subcategory-option';
noneDiv.setAttribute('data-parent-id', parentId);
noneDiv.setAttribute('data-category-id', `${parentId}_none`);
noneDiv.setAttribute('data-thematic', parentOption ? parentOption.getAttribute('data-thematic') : '');
noneDiv.innerHTML = `<input type="checkbox" name="category" id="${noneId}" value="${parentId}">` +
`<label for="${noneId}"><i class="bi bi-slash-circle me-1 text-muted small"></i>${typeof subcatNoneLabel !== 'undefined' ? subcatNoneLabel : '(aucune sous-catégorie)'}</label>`;
allChildren[0].insertAdjacentElement('beforebegin', noneDiv);
// Determine if group should be expanded on page load
const anyChildSelected = allChildren.some(ch => {
const cb = ch.querySelector('input[type="checkbox"]');
return cb && urlCategories.includes(cb.value);
});
const parentDirectlySelected = urlCategories.includes(parentId);
const expand = anyChildSelected || parentDirectlySelected;
if (expand) {
if (parentCb) parentCb.checked = true;
noneDiv.style.display = '';
if (parentDirectlySelected) noneDiv.querySelector('input').checked = true;
allChildren.forEach(ch => { ch.style.display = ''; });
} else {
noneDiv.style.display = 'none';
allChildren.forEach(ch => {
ch.style.display = 'none';
const cb = ch.querySelector('input[type="checkbox"]');
if (cb) cb.checked = false;
});
}
// Add indent margin to first child in group (after noneDiv)
if (allChildren[0]) allChildren[0].style.marginLeft = '0.5rem';
// Tous/Aucun controls after last child
const controls = document.createElement('div');
controls.className = 'filter-subcat-controls';
controls.setAttribute('data-for-parent', parentId);
controls.style.display = expand ? '' : 'none';
controls.style.marginLeft = '0.5rem';
controls.innerHTML = `<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 subcat-select-all" data-parent="${parentId}">Tous</button>` +
`<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 subcat-deselect-all" data-parent="${parentId}">Aucun</button>`;
allChildren[allChildren.length - 1].insertAdjacentElement('afterend', controls);
controls.querySelector('.subcat-select-all').addEventListener('click', function(e) {
e.preventDefault();
catContainer.querySelectorAll(`.filter-option[data-parent-id="${parentId}"]`).forEach(opt => {
if (opt.style.display !== 'none') {
const cb = opt.querySelector('input[type="checkbox"]');
if (cb) cb.checked = true;
}
});
});
controls.querySelector('.subcat-deselect-all').addEventListener('click', function(e) {
e.preventDefault();
catContainer.querySelectorAll(`.filter-option[data-parent-id="${parentId}"]`).forEach(opt => {
const cb = opt.querySelector('input[type="checkbox"]');
if (cb) cb.checked = false;
});
});
});
}
});
function filterCategoriesByThematics() {
// Get all checked thematics
const checkedThematics = Array.from(document.querySelectorAll('input[name="thematic"]:checked'))
.map(cb => cb.value);
// Get all category options
const categoryOptions = document.querySelectorAll('.filter-options[data-field="category"] .filter-option');
categoryOptions.forEach(option => {
const parentId = option.getAttribute('data-parent-id');
if (parentId) {
// Sub-category: only show if parent is visible AND checked
const parentOption = document.querySelector(`.filter-options[data-field="category"] .filter-option[data-category-id="${parentId}"]`);
const parentVisible = parentOption && parentOption.style.display !== 'none';
const parentChecked = parentOption && parentOption.querySelector('input[type="checkbox"]')?.checked;
if (!parentVisible || !parentChecked) {
option.style.display = 'none';
const cb = option.querySelector('input[type="checkbox"]');
if (cb) cb.checked = false;
}
return;
}
// Parent category: show/hide based on thematic
if (checkedThematics.length === 0) {
option.style.display = '';
return;
}
const categoryThematic = option.getAttribute('data-thematic');
if (categoryThematic && checkedThematics.includes(categoryThematic)) {
option.style.display = '';
} else if (categoryThematic) {
option.style.display = 'none';
const checkbox = option.querySelector('input[type="checkbox"]');
if (checkbox) checkbox.checked = false;
// Hide children and controls too
const catId = option.getAttribute('data-category-id');
if (catId) {
const controls = document.querySelector(`.filter-subcat-controls[data-for-parent="${catId}"]`);
if (controls) controls.style.display = 'none';
document.querySelectorAll(`.filter-options[data-field="category"] .filter-option[data-parent-id="${catId}"]`).forEach(child => {
child.style.display = 'none';
const childCb = child.querySelector('input[type="checkbox"]');
if (childCb) childCb.checked = false;
});
}
} else {
option.style.display = '';
}
});
}
function filterProvidersAndContracts() {
const providerOptions = document.querySelectorAll('.filter-options[data-field="provider"] .filter-option');
const contractOptions = document.querySelectorAll('.filter-options[data-field="contract"] .filter-option');
const memberOptions = document.querySelectorAll('.filter-options[data-field="assigned_member"] .filter-option');
const teamOptions = document.querySelectorAll('.filter-options[data-field="assigned_team"] .filter-option');
if (!providerOptions.length && !contractOptions.length && !memberOptions.length && !teamOptions.length) {
return;
}
const selectedThematics = Array.from(document.querySelectorAll('input[name="thematic"]:checked')).map(cb => cb.value);
const selectedProviders = Array.from(document.querySelectorAll('input[name="provider"]:checked')).map(cb => cb.value);
const selectedContracts = Array.from(document.querySelectorAll('input[name="contract"]:checked')).map(cb => cb.value);
const selectedContractProviders = Array.from(document.querySelectorAll('input[name="contract"]:checked')).map(cb => cb.closest('.filter-option')?.dataset?.provider || '').filter(Boolean);
const allowedCompanies = Array.from(new Set([...selectedProviders, ...selectedContractProviders].filter(Boolean)));
const optionMatchesThematics = (option) => {
if (!selectedThematics.length) {
return true;
}
const codes = (option?.dataset?.thematicCodes || '').split(',').map(x => x.trim()).filter(Boolean);
if (!codes.length) {
return true;
}
return codes.some(code => selectedThematics.includes(code));
};
providerOptions.forEach(option => {
const checkbox = option.querySelector('input[type="checkbox"]');
const visible = optionMatchesThematics(option);
option.style.display = visible ? '' : 'none';
if (!visible && checkbox) {
checkbox.checked = false;
}
});
contractOptions.forEach(option => {
const checkbox = option.querySelector('input[type="checkbox"]');
const matchesThematic = optionMatchesThematics(option);
const optionProvider = option.dataset.provider || '';
const matchesProvider = !selectedProviders.length || selectedProviders.includes(optionProvider);
const visible = matchesThematic && matchesProvider;
option.style.display = visible ? '' : 'none';
if (!visible && checkbox) {
checkbox.checked = false;
}
});
// Filter teams by allowed companies and thematics
teamOptions.forEach(option => {
const checkbox = option.querySelector('input[type="checkbox"]');
const optionCompany = option.dataset.company || '';
const matchesCompany = !allowedCompanies.length || allowedCompanies.includes(optionCompany);
const visible = matchesCompany && optionMatchesThematics(option);
option.style.display = visible ? '' : 'none';
if (!visible && checkbox) {
checkbox.checked = false;
}
});
// Optional: filter members when teams are selected
const selectedTeams = Array.from(document.querySelectorAll('input[name="assigned_team"]:checked')).map(cb => cb.value);
if (memberOptions.length) {
memberOptions.forEach(option => {
const checkbox = option.querySelector('input[type="checkbox"]');
const optionTeams = (option.dataset.team || '').split(',').map(x => x.trim()).filter(Boolean);
const optionCompanies = (option.dataset.company || '').split(',').map(x => x.trim()).filter(Boolean);
const matchesCompany = !allowedCompanies.length || optionCompanies.some(companyId => allowedCompanies.includes(companyId));
const matchesTeam = !selectedTeams.length || optionTeams.some(teamId => selectedTeams.includes(teamId));
const visible = matchesCompany && matchesTeam && optionMatchesThematics(option);
option.style.display = visible ? '' : 'none';
if (!visible && checkbox) {
checkbox.checked = false;
}
});
}
if (typeof updateInterventionManagers === 'function') {
updateInterventionManagers();
}
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name + '=')) {
cookieValue = decodeURIComponent(cookie.slice(name.length + 1));
break;
}
}
}
return cookieValue;
}
// ==================== Column Visibility Persistence ====================
function _getHiddenColumns($table) {
try {
const allCols = $table.bootstrapTable('getOptions').columns[0] || [];
const visibleFields = $table.bootstrapTable('getVisibleColumns').map((c) => c.field);
return allCols
.filter((c) => c.field && c.field !== 'actions' && !visibleFields.includes(c.field))
.map((c) => c.field);
} catch (e) {
return [];
}
}
function _saveColumnVisibility($table) {
if (typeof savePreferencesUrl === 'undefined') return;
const hiddenFields = _getHiddenColumns($table);
const filters = getCurrentFilters();
const sort = getCurrentSort();
fetch(savePreferencesUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ filters: filters, sort: sort, columns: hiddenFields })
}).catch(err => console.warn('Could not save column preferences:', err));
}
function _restoreColumnVisibility($table) {
try {
// null = jamais sauvegardé, on laisse les défauts de la définition des colonnes
if (typeof SAVED_COLUMNS === 'undefined' || SAVED_COLUMNS === null) return;
if (!Array.isArray(SAVED_COLUMNS)) return;
// On récupère tous les champs définis (hors 'actions')
const allCols = $table.bootstrapTable('getOptions').columns[0] || [];
allCols.forEach((col) => {
if (!col.field || col.field === 'actions') return;
if (SAVED_COLUMNS.includes(col.field)) {
$table.bootstrapTable('hideColumn', col.field);
} else {
$table.bootstrapTable('showColumn', col.field);
}
});
} catch (e) {
// ignore
}
}
// ==================== Filter and Sort Preferences Management ====================
/**
* Count active filters from URL parameters
*/
function countActiveFilters() {
const params = new URLSearchParams(window.location.search);
let count = 0;
// List of filter parameters to count (each distinct parameter counts as 1 filter)
const filterParams = {
'thematic': false,
'status': false,
'priority': false,
'maintain_type': false,
'category': false,
'type': false,
'provider': false,
'contract': false,
'assigned_team': false,
'assigned_member': false,
'intervention_manager': false,
'pause_reason': false,
'begin_date_min': false,
'begin_date_max': false,
'end_date_min': false,
'end_date_max': false,
'locations': false,
'overdue': false,
'to_be_planned_soon': false
};
// Check each parameter
for (const param in filterParams) {
if (params.has(param) && params.get(param) !== '') {
const values = params.getAll(param);
if (values.length > 0 && values.some(v => v !== '')) {
count++;
}
}
}
return count;
}
/**
* Update the filter badge with the number of active filters
*/
function updateFilterBadge() {
const count = countActiveFilters();
const badge = document.getElementById('filter-badge');
const countSpan = document.getElementById('filter-count');
if (badge && countSpan) {
if (count > 0) {
countSpan.textContent = count;
badge.classList.remove('d-none');
} else {
badge.classList.add('d-none');
}
}
}
/**
* Extract current filters from URL
*/
function getCurrentFilters() {
const params = new URLSearchParams(window.location.search);
const filters = {};
// Get all filter parameters
const filterKeys = [
'thematic', 'status', 'priority', 'maintain_type', 'category',
'type', 'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager',
'pause_reason', 'locations', 'overdue', 'to_be_planned_soon'
];
filterKeys.forEach(key => {
const values = params.getAll(key);
if (values.length > 0) {
filters[key] = values.length === 1 ? values[0] : values;
}
});
// Date filters (single values)
['begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max'].forEach(key => {
const value = params.get(key);
if (value) {
filters[key] = value;
}
});
return filters;
}
/**
* Get current sort settings from the table
*/
function getCurrentSort() {
const $table = $('#interventions-table');
if (!$table.length || typeof $table.bootstrapTable !== 'function') {
return {};
}
try {
const options = $table.bootstrapTable('getOptions') || {};
return {
sortName: options.sortName || null,
sortOrder: options.sortOrder || null
};
} catch (error) {
console.warn('Unable to get current sort:', error);
return {};
}
}
/**
* Save filter and sort preferences to backend
*/
function savePreferences() {
if (typeof savePreferencesUrl === 'undefined') return;
const filters = getCurrentFilters();
const sort = getCurrentSort();
const $table = $('#interventions-table');
const columns = $table.length && $table.data('bootstrap.table') ? _getHiddenColumns($table) : undefined;
const payload = { filters: filters, sort: sort };
if (columns !== undefined) payload.columns = columns;
fetch(savePreferencesUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify(payload)
}).catch(err => console.warn('Could not save preferences:', err));
}
// ==================== Filter and Sort Preferences Management ====================
// Count active filters from URL parameters
function countActiveFilters() {
const urlParams = new URLSearchParams(window.location.search);
let count = 0;
// Define filter parameters used in the global filter modal
const filterParams = [
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
'overdue', 'to_be_planned_soon'
];
filterParams.forEach(param => {
const values = urlParams.getAll(param);
if (values.length > 0 && values[0] !== '') {
count++; // Each filter type counts as 1, regardless of number of selected values
}
});
return count;
}
// Update filter badge display
function updateFilterBadge() {
const count = countActiveFilters();
const badge = document.getElementById('filter-badge');
const countSpan = document.getElementById('filter-count');
if (badge && countSpan) {
if (count > 0) {
countSpan.textContent = count;
badge.classList.remove('d-none');
} else {
badge.classList.add('d-none');
}
}
}
// Save preferences from URL parameters (used when filters are already applied)
function savePreferencesFromUrl() {
if (typeof savePreferencesUrl === 'undefined') {
return;
}
// Get current filters from URL
const urlParams = new URLSearchParams(window.location.search);
const filters = {};
// Collect all filter parameters
const filterParams = [
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
'overdue', 'to_be_planned_soon'
];
filterParams.forEach(param => {
const values = urlParams.getAll(param);
if (values.length > 1) {
filters[param] = values;
} else if (values.length === 1 && values[0] !== '') {
filters[param] = values[0];
}
});
// Get current sort from table
const $table = $('#interventions-table');
let sort = {};
if ($table.length && $table.data('bootstrap.table')) {
const options = $table.bootstrapTable('getOptions');
if (options.sortName) {
sort = {
sortName: options.sortName,
sortOrder: options.sortOrder || 'asc'
};
}
}
// Send to server
const columns = $table.length && $table.data('bootstrap.table') ? _getHiddenColumns($table) : undefined;
const payload = { filters, sort };
if (columns !== undefined) payload.columns = columns;
fetch(savePreferencesUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify(payload)
}).catch(() => {});
}
// Save preferences from form data (used when applying new filters)
async function savePreferencesFromForm(urlParams) {
if (typeof savePreferencesUrl === 'undefined') {
return;
}
const filters = {};
// Collect all filter parameters from URLSearchParams
const filterParams = [
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
'overdue', 'to_be_planned_soon'
];
filterParams.forEach(param => {
const values = urlParams.getAll(param);
if (values.length > 1) {
filters[param] = values;
} else if (values.length === 1 && values[0] !== '') {
filters[param] = values[0];
}
});
// Get current sort from table (if available)
const $table = $('#interventions-table');
let sort = {};
if ($table.length && $table.data('bootstrap.table')) {
const options = $table.bootstrapTable('getOptions');
if (options.sortName) {
sort = {
sortName: options.sortName,
sortOrder: options.sortOrder || 'asc'
};
}
}
// Send to server with keepalive to ensure request completes even after redirect
const $tableForm = $('#interventions-table');
const columnsForm = $tableForm.length && $tableForm.data('bootstrap.table') ? _getHiddenColumns($tableForm) : undefined;
const payloadForm = { filters, sort };
if (columnsForm !== undefined) payloadForm.columns = columnsForm;
try {
await fetch(savePreferencesUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify(payloadForm),
keepalive: true
});
} catch (err) {
// Silently fail
}
}
// ==================== Initialization ====================
// Initialize filter badge and sort on page load
document.addEventListener('DOMContentLoaded', function() {
updateFilterBadge();
// Save preferences when table sort changes (user action only)
const $table = $('#interventions-table');
if ($table.length) {
$table.on('sort.bs.table', function() {
setTimeout(savePreferencesFromUrl, 500); // Debounce and save after sort is applied
});
}
});
// Update badge after page fully loads (including after filter redirect)
window.addEventListener('load', function() {
updateFilterBadge();
// Restore search input from URL if present
const _urlParams = new URLSearchParams(window.location.search);
const _urlQ = _urlParams.get('q') || '';
if (_urlQ) {
$('#interventions-search-input').val(_urlQ);
$('#interventions-search-clear').css('visibility', 'visible');
const _urlMode = _urlParams.get('search_mode') || 'and';
const _radio = document.querySelector(`input[name="search-mode"][value="${_urlMode}"]`);
if (_radio) _radio.checked = true;
}
});
// === Search service (table + map) ===
function _buildSearchParams() {
// Start from current URL filter params, then overlay search terms
const params = new URLSearchParams(window.location.search);
const q = $('#interventions-search-input').val().trim();
if (q) {
params.set('q', q);
params.set('search_mode', document.querySelector('input[name="search-mode"]:checked')?.value || 'and');
} else {
params.delete('q');
params.delete('search_mode');
}
return params;
}
function runInterventionSearch() {
const params = _buildSearchParams();
const hasSearch = !!$('#interventions-search-input').val().trim();
$('#interventions-search-clear').css('visibility', hasSearch ? 'visible' : 'hidden');
handleGeojsonMapLoading(false, interventionsgeojsonloader, 'interventions');
interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl + '?' + params);
}
$('#interventions-search-btn').on('click', function () {
runInterventionSearch();
});
$('#interventions-search-input').on('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
runInterventionSearch();
} else if (e.key === 'Escape') {
$(this).val('');
runInterventionSearch();
}
});
$('#interventions-search-clear').on('click', function () {
$('#interventions-search-input').val('');
$('#interventions-search-clear').css('visibility', 'hidden');
runInterventionSearch();
});
// Update badge when returning from filter modal
const filterModal = document.getElementById('filterModal');
if (filterModal) {
filterModal.addEventListener('hidden.bs.modal', function() {
// Badge will be updated on page reload after filter application
});
}
function updateInterventionManagers() {
const checkedContracts = document.querySelectorAll('.filter-options[data-field="contract"] input[type="checkbox"]:checked');
const container = document.getElementById('intervention-manager-options-container');
const row = document.getElementById('intervention-manager-filter-row');
if (!container || !row) return;
// Get currently selected managers from URL or from checkboxes if they already exist
const urlParams = new URLSearchParams(window.location.search);
const selectedManagers = new Set(urlParams.getAll('intervention_manager'));
// Also check if any are currently checked in the UI before re-drawing
const existingChecked = container.querySelectorAll('input[type="checkbox"]:checked');
existingChecked.forEach(cb => selectedManagers.add(cb.value));
// Gather unique managers
const managersMap = new Map();
let anyUsesManager = false;
checkedContracts.forEach(cb => {
const optionDiv = cb.closest('.filter-option');
if (optionDiv) {
const usesManager = optionDiv.getAttribute('data-uses-manager') === 'true';
if (usesManager) {
anyUsesManager = true;
const managersStr = optionDiv.getAttribute('data-managers');
if (managersStr) {
try {
const managers = JSON.parse(managersStr);
managers.forEach(mgr => {
managersMap.set(String(mgr.id), mgr.name);
});
} catch (e) {
console.error("Error parsing managers JSON", e);
}
}
}
}
});
if (anyUsesManager && managersMap.size > 0) {
// Build the HTML for options
let html = '';
managersMap.forEach((name, id) => {
const isChecked = selectedManagers.has(id) ? 'checked' : '';
html += `
<div class="filter-option">
<input type="checkbox" name="intervention_manager" id="intervention_manager_${id}" value="${id}" ${isChecked}>
<label for="intervention_manager_${id}">${name}</label>
</div>
`;
});
container.innerHTML = html;
row.classList.remove('d-none');
} else {
container.innerHTML = '';
row.classList.add('d-none');
}
}