2661 lines
104 KiB
JavaScript
2661 lines
104 KiB
JavaScript
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 += ` <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');
|
||
}
|
||
}
|