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

1090 lines
37 KiB
JavaScript

let observations_geojson = {};
let allObservations = [];
let visibleRows = [];
const MAX_ITEMS = 5000;
const DESCRIPTION_MAX_LENGTH = 60;
function truncateText(text, limit) {
if (!text || text.length <= limit) {
return text;
}
return `${text.substring(0, limit - 3).trim()}...`;
}
const STATUS_METADATA = {
in_preparation: { label: gettext('En préparation'), badge: 'secondary', color: '#6c757d' },
to_process: { label: gettext('À traiter'), badge: 'danger', color: '#dc3545' },
in_progress: { label: gettext('En cours'), badge: 'warning', color: '#ffc107' },
processed: { label: gettext('Traité'), badge: 'success', color: '#198754' },
not_relevant: { label: gettext('Non relevant'), badge: 'secondary', color: '#6c757d' },
to_redirect: { label: gettext('À rediriger'), badge: 'warning', color: '#ffc107' },
duplicate: { label: gettext('Doublon'), badge: 'info', color: '#0dcaf0' },
closed: { label: gettext('Clôturé'), badge: 'dark', color: '#343a40' },
archived: { label: gettext('Archivé'), badge: 'dark', color: '#343a40' }
};
function getStatusMetadata(status) {
return STATUS_METADATA[status] || { label: status || '-', badge: 'secondary', color: '#888888' };
}
const OBSERVATION_TYPE_METADATA = {
incident: { label: gettext('Incident'), badge: 'danger' },
inventory: { label: gettext('Inventaire'), badge: 'secondary' },
remark: { label: gettext('Remarque'), badge: 'info' },
};
function getObservationTypeMetadata(type) {
return OBSERVATION_TYPE_METADATA[type] || { label: type || '-', badge: 'secondary' };
}
const observationsgeojsonloader = new GeojsonLoader();
const observations_layer_defs = [
{
id: 'observations',
type: 'circle',
source: 'observations',
minzoom: 0,
maxzoom: 24,
paint: {
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
8, 4,
14, 8,
18, 14
],
'circle-color': [
'match',
['get', 'status'],
'in_preparation', STATUS_METADATA.in_preparation.color,
'to_process', STATUS_METADATA.to_process.color,
'in_progress', STATUS_METADATA.in_progress.color,
'processed', STATUS_METADATA.processed.color,
'closed', STATUS_METADATA.closed.color,
'archived', STATUS_METADATA.archived.color,
'#007cbf'
],
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 1.5,
'circle-opacity': 0.85
}
}
];
const highlightState = { previousId: null };
document.addEventListener('DOMContentLoaded', () => {
maploader.addEventListener('mapLoaded', registerMapInteractions, { once: true });
maploader.loadMap('map');
handleGeojsonMapLoading(true, observationsgeojsonloader, 'observations', observations_layer_defs);
initObservationsTable();
setupColumnToggles();
setupFilterForm();
setupFilterZoneButton();
setupLayoutButtons();
const query = new URLSearchParams(window.location.search).toString();
const initialUrl = query ? `${observationsgeojsonUrl}?${query}` : observationsgeojsonUrl;
observationsgeojsonloader.loadGeojson(initialUrl);
// Sync export button with initial URL filters
_updateExportExcelUrl(query);
});
observationsgeojsonloader.addEventListener('geojsonLoaded', (event) => {
const geojsonData = event.detail;
observations_geojson = geojsonData;
allObservations = geojsonData.features || [];
visibleRows = [...allObservations];
loadDataIntoObservationsTable(allObservations);
fitMapToObservations(allObservations);
updateFilterLimitAlert(allObservations.length);
});
observationsgeojsonloader.addEventListener('geojsonError', () => {
document.getElementById('observations-spinner')?.classList.add('d-none');
document.getElementById('observations-table-container')?.classList.remove('d-none');
updateResultCount(0);
});
function registerMapInteractions(event) {
const mapInstance = event.detail;
mapInstance.addControl(new maplibregl.NavigationControl(), 'top-right');
// Contrôle photo aérienne
if (typeof LayerSwitcherControl !== 'undefined') {
const layerSwitcher = new LayerSwitcherControl({
layers: [
{
id: 'ortho',
name: 'Orthophotos',
icon: 'satellite',
group: 'ortho',
beforeLayer: 'observations',
layer: {
type: 'raster',
paint: { 'raster-opacity': 1 }
},
sublayers: [
{
id: 'urbis',
name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)',
default: true,
source: {
type: 'raster',
tiles: [
window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}'
],
tileSize: 256,
attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '\u00a9 Brussels UrbIS'
}
},
{
id: 'vlaanderen',
name: 'Vlaanderen 2023',
source: {
type: 'raster',
tiles: [
'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'
],
tileSize: 256,
attribution: '\u00a9 Digitaal Vlaanderen - Orthofoto 2023'
}
}
]
}
]
});
mapInstance.addControl(layerSwitcher, 'top-left');
}
mapInstance.on('click', 'observations', (e) => {
const feature = e.features?.[0];
if (!feature) {
return;
}
highlightMapFeature(mapInstance, 'observations', feature.id, highlightState);
const props = feature.properties || {};
const statusMeta = getStatusMetadata(props.status);
const detailUrl = `${observationsIndexUrl}${props.id}/`;
const popupContent = `
<div class="popup-scrollable">
<strong><a href="${detailUrl}">${props.description || gettext('Observation')}</a></strong>
<div class="mb-1">${props.address || ''}</div>
<div class="mb-1"><span class="badge text-bg-${statusMeta.badge}">${props.status_label || statusMeta.label}</span></div>
<div class="text-muted small">${dateFormatter(props.created_at)}</div>
</div>
`;
new maplibregl.Popup({ closeButton: true })
.setLngLat(e.lngLat)
.setHTML(popupContent)
.addTo(mapInstance);
});
mapInstance.on('mouseenter', 'observations', () => {
mapInstance.getCanvas().style.cursor = 'pointer';
});
mapInstance.on('mouseleave', 'observations', () => {
mapInstance.getCanvas().style.cursor = '';
});
}
function initObservationsTable() {
const $table = $('#observations-table');
if (!$table.length) {
return;
}
function calculateTableHeight() {
const windowHeight = $(window).height();
const offset = 260;
return Math.max(360, windowHeight - offset);
}
// 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';
} else {
initialSort.sortName = 'created_at';
initialSort.sortOrder = 'desc';
}
$table.bootstrapTable({
columns: [
{ field: 'actions', title: '', formatter: 'actionsFormatter', class: 'text-nowrap' },
{ field: 'code', title: gettext('Code'), formatter: 'codeFormatter', class: 'text-nowrap', sortable: true },
{ field: 'thematic', title: gettext('Thématique'), formatter: 'thematicFormatter', class: 'text-nowrap', sortable: true },
{ field: 'category', title: gettext('Catégorie'), sortable: true },
{ field: 'symptom', title: gettext('Symptôme'), sortable: true },
{ field: 'observation_type_label', title: gettext('Type'), formatter: 'observationTypeFormatter', class: 'text-nowrap', sortable: true },
{ field: 'status_label', title: gettext('Statut'), formatter: 'statusFormatter', class: 'text-nowrap', sortable: true },
{ field: 'created_at', title: gettext('Date'), formatter: 'dateFormatter', class: 'text-nowrap', sortable: true },
{ field: 'description', title: gettext('Description'), formatter: 'descriptionFormatter', sortable: true },
{ field: 'address', title: gettext('Adresse'), sortable: true }
],
height: calculateTableHeight(),
...initialSort
});
// Restore saved column visibility
_restoreColumnVisibility($table);
// Save column visibility when toggled
$table.on('column-switch.bs.table', function () {
setTimeout(function () {
_saveColumnVisibility($table);
$table.bootstrapTable('resetView');
}, 0);
});
// Save sort preferences when sort changes
$table.on('sort.bs.table', function () {
setTimeout(savePreferencesFromUrl, 500);
});
registerZoomAction('#observations-table', '.zoom-btn', map, 16);
$table.on('post-body.bs.table', () => {
visibleRows = $('#observations-table').bootstrapTable('getData');
syncMapWithTable();
});
$(window).on('resize', () => {
$table.bootstrapTable('resetView', { height: calculateTableHeight() });
});
}
function loadDataIntoObservationsTable(features) {
const tableData = features.map((feature) => {
const props = feature.properties || {};
const coords = feature.geometry?.coordinates || [];
const statusMeta = getStatusMetadata(props.status);
return {
actions: '',
id: props.id || feature.id,
code: props.code || '',
description: props.description || '-',
thematic: props.thematic || '-',
thematic_icon: props.thematic_icon || '',
observation_type: props.observation_type || '',
observation_type_label: props.observation_type_label || '-',
status: props.status || '',
status_label: props.status_label || statusMeta.label,
status_badge: statusMeta.badge,
category: props.category || '-',
symptom: props.symptom || '-',
address: props.address || '-',
created_at: props.created_at || '',
lon: coords[0] ?? null,
lat: coords[1] ?? null
};
});
$('#observations-spinner').addClass('d-none');
$('#observations-table-container').removeClass('d-none');
$('#observations-table').bootstrapTable('load', tableData);
visibleRows = tableData;
syncMapWithTable();
updateResultCount(tableData.length);
}
function updateFilterLimitAlert(count) {
const alertEl = document.getElementById('filter-limit-alert');
if (!alertEl) {
return;
}
const reached = count >= MAX_ITEMS;
alertEl.classList.toggle('d-none', !reached);
if (reached) {
alertEl.title = interpolate(gettext('Résultat tronqué à 5 000 éléments (%(count)s reçus).'), { count }, true);
} else {
alertEl.removeAttribute('title');
}
}
function updateResultCount(count) {
const target = document.getElementById('observations-result-count');
if (!target) {
return;
}
if (!count) {
target.textContent = gettext('Aucune observation chargée');
return;
}
const msg = ngettext('%(count)s observation chargée', '%(count)s observations chargées', count);
target.textContent = interpolate(msg, { count }, true);
}
function fitMapToObservations(features) {
if (!map || !features.length) {
return;
}
const bounds = new maplibregl.LngLatBounds();
features.forEach((feature) => {
const coords = feature.geometry?.coordinates;
if (coords && coords.length === 2) {
bounds.extend(coords);
}
});
if (!bounds.isEmpty()) {
map.fitBounds(bounds, { padding: 50 });
}
}
function setupFilterForm() {
const form = document.getElementById('filter-form');
if (!form) {
return;
}
// Apply filters button
document.getElementById('apply-filters')?.addEventListener('click', () => {
const params = buildFilterParams(new FormData(form));
_applySearchToParams(params);
savePreferencesFromForm(params);
refreshListing(params);
const filterModal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
if (filterModal) filterModal.hide();
});
// Reset filters button
document.getElementById('reset-filters')?.addEventListener('click', () => {
// Uncheck all filter checkboxes
form.querySelectorAll('input[type="checkbox"][name="thematic"]').forEach(cb => { cb.checked = false; });
form.querySelectorAll('input[type="checkbox"][name="category"]').forEach(cb => { cb.checked = false; });
form.querySelectorAll('input[type="checkbox"][name="symptom"]').forEach(cb => { cb.checked = false; });
// Reset type checkboxes: check all except 'inventory'
form.querySelectorAll('input[type="checkbox"][name="type"]').forEach(cb => {
cb.checked = cb.value !== 'inventory';
});
// Reset status checkboxes: check all except 'archived'
form.querySelectorAll('input[type="checkbox"][name="status"]').forEach(cb => {
cb.checked = cb.value !== 'archived';
});
// Reset dates and other fields
const dateFrom = form.querySelector('input[name="date_from"]');
const dateTo = form.querySelector('input[name="date_to"]');
if (dateFrom) dateFrom.value = '';
if (dateTo) dateTo.value = '';
const mineCheck = form.querySelector('input[name="mine"]');
const teamCheck = form.querySelector('input[name="team"]');
if (mineCheck) mineCheck.checked = false;
if (teamCheck) teamCheck.checked = false;
// Show all category and symptom options after reset — collapse sub-category groups
document.querySelectorAll('.filter-options[data-field="category"] .filter-option[data-parent-id]:not([data-parent-id=""])').forEach(ch => {
ch.style.display = 'none';
});
document.querySelectorAll('.filter-subcat-controls').forEach(c => { c.style.display = 'none'; });
filterCategoriesByThematics();
filterSymptomsByThematicsAndCategories();
const params = buildFilterParams(new FormData(form));
_applySearchToParams(params);
// Save empty preferences on reset
if (typeof savePreferencesUrl !== 'undefined') {
fetch(savePreferencesUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') },
body: JSON.stringify({ filters: {}, sort: {} })
}).catch(() => {});
}
refreshListing(params);
const filterModal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
if (filterModal) filterModal.hide();
});
// Select-all / deselect-all buttons
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) {
container.querySelectorAll('.filter-option:not([style*="display: none"]) input[type="checkbox"]').forEach(cb => {
cb.checked = true;
});
}
});
});
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) {
container.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
if (field === 'thematic') {
filterCategoriesByThematics();
filterSymptomsByThematicsAndCategories();
}
if (field === 'category') {
filterSymptomsByThematicsAndCategories();
}
}
});
});
// Thematic → category → symptom chain
document.querySelectorAll('input[name="thematic"]').forEach(cb => {
cb.addEventListener('change', () => {
filterCategoriesByThematics();
filterSymptomsByThematicsAndCategories();
});
});
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;
}
});
}
}
filterSymptomsByThematicsAndCategories();
});
});
// Create (aucune) chip + Tous/Aucun buttons + 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;
});
}
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;
});
});
});
}
// Initialize filters on load
filterCategoriesByThematics();
filterSymptomsByThematicsAndCategories();
}
function filterCategoriesByThematics() {
const checkedThematics = Array.from(document.querySelectorAll('input[name="thematic"]:checked')).map(cb => cb.value);
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 optThematic = option.getAttribute('data-thematic');
if (optThematic && checkedThematics.includes(optThematic)) {
option.style.display = '';
} else if (optThematic) {
option.style.display = 'none';
const cb = option.querySelector('input[type="checkbox"]');
if (cb) cb.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 filterSymptomsByThematicsAndCategories() {
const checkedThematics = Array.from(document.querySelectorAll('input[name="thematic"]:checked')).map(cb => cb.value);
const checkedCategories = Array.from(document.querySelectorAll('input[name="category"]:checked')).map(cb => cb.value);
const symptomOptions = document.querySelectorAll('.filter-options[data-field="symptom"] .filter-option');
symptomOptions.forEach(option => {
const optThematic = option.getAttribute('data-thematic') || '';
const optCats = (option.getAttribute('data-categories') || '').split(',').map(s => s.trim()).filter(Boolean);
const matchesThematic = !checkedThematics.length || (optThematic && checkedThematics.includes(optThematic));
let matchesCategory;
if (!checkedCategories.length) {
matchesCategory = true;
} else if (!optCats.length) {
matchesCategory = true;
} else {
matchesCategory = optCats.some(c => checkedCategories.includes(c));
}
if (matchesThematic && matchesCategory) {
option.style.display = '';
} else {
option.style.display = 'none';
const cb = option.querySelector('input[type="checkbox"]');
if (cb) cb.checked = false;
}
});
}
function buildFilterParams(formData) {
const params = new URLSearchParams();
for (const [key, value] of formData.entries()) {
if (value) {
params.append(key, value);
}
}
return params;
}
function refreshListing(params) {
const queryString = params.toString();
const targetUrl = queryString ? `${observationsgeojsonUrl}?${queryString}` : observationsgeojsonUrl;
window.history.pushState({}, '', queryString ? `${window.location.pathname}?${queryString}` : window.location.pathname);
_updateExportExcelUrl(queryString);
$('#observations-spinner').removeClass('d-none');
$('#observations-table-container').addClass('d-none');
handleGeojsonMapLoading(false, observationsgeojsonloader, 'observations');
observationsgeojsonloader.loadGeojson(targetUrl);
}
function _updateExportExcelUrl(queryString) {
const btn = document.getElementById('export-excel-btn');
if (!btn || typeof observationsExportExcelUrl === 'undefined') return;
btn.href = queryString ? `${observationsExportExcelUrl}?${queryString}` : observationsExportExcelUrl;
}
// === Search widget ===
function _applySearchToParams(params) {
const q = $('#observations-search-input').val().trim();
if (q) {
params.set('q', q);
params.set('search_mode', document.querySelector('input[name="obs-search-mode"]:checked')?.value || 'and');
} else {
params.delete('q');
params.delete('search_mode');
}
}
function _buildObsSearchParams() {
const params = new URLSearchParams(window.location.search);
_applySearchToParams(params);
return params;
}
function runObservationSearch() {
const params = _buildObsSearchParams();
const hasSearch = !!$('#observations-search-input').val().trim();
$('#observations-search-clear').css('visibility', hasSearch ? 'visible' : 'hidden');
refreshListing(params);
setTimeout(savePreferencesFromUrl, 500);
}
$('#observations-search-btn').on('click', function () {
runObservationSearch();
});
$('#observations-search-input').on('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
runObservationSearch();
} else if (e.key === 'Escape') {
$(this).val('');
runObservationSearch();
}
});
$('#observations-search-clear').on('click', function () {
$('#observations-search-input').val('');
$('#observations-search-clear').css('visibility', 'hidden');
runObservationSearch();
});
window.addEventListener('load', function () {
const _urlParams = new URLSearchParams(window.location.search);
const _urlQ = _urlParams.get('q') || '';
if (_urlQ) {
$('#observations-search-input').val(_urlQ);
$('#observations-search-clear').css('visibility', 'visible');
const _urlMode = _urlParams.get('search_mode') || 'and';
const _radio = document.querySelector(`input[name="obs-search-mode"][value="${_urlMode}"]`);
if (_radio) _radio.checked = true;
}
});
function setupFilterZoneButton() {
const button = document.getElementById('filterzone');
if (!button) {
return;
}
button.addEventListener('click', (event) => {
event.preventDefault();
if (!map || !observations_geojson.features) {
return;
}
filterTableByMapBounds(map, observations_geojson, loadDataIntoObservationsTable);
});
}
function setupColumnToggles() {
const colLeft = document.getElementById('col-left');
const colRight = document.getElementById('col-right');
const btnLeft = document.getElementById('expand-col-left');
const btnRight = document.getElementById('expand-col-right');
if (!colLeft || !colRight || !btnLeft || !btnRight) {
return;
}
const iconLeft = btnLeft.querySelector('i');
const iconRight = btnRight.querySelector('i');
function resetColumns() {
colLeft.classList.remove('col-11', 'order-2');
colRight.classList.remove('col-11', 'order-2');
colLeft.classList.add('col-md-6');
colRight.classList.add('col-md-6');
btnLeft.setAttribute('aria-expanded', 'false');
btnRight.setAttribute('aria-expanded', 'false');
iconLeft.classList.remove('bi-box-arrow-in-left');
iconLeft.classList.add('bi-box-arrow-in-right');
iconRight.classList.remove('bi-box-arrow-in-right');
iconRight.classList.add('bi-box-arrow-in-left');
$('#observations-table').bootstrapTable('resetView');
map?.resize();
}
btnLeft.addEventListener('click', () => {
const expanded = btnLeft.getAttribute('aria-expanded') === 'true';
if (!expanded) {
colLeft.classList.remove('col-md-6', 'order-2');
colRight.classList.remove('col-md-6', 'order-2');
colLeft.classList.add('col-11');
colRight.classList.add('col-11', 'order-2');
btnLeft.setAttribute('aria-expanded', 'true');
btnRight.setAttribute('aria-expanded', 'false');
iconLeft.classList.remove('bi-box-arrow-in-right');
iconLeft.classList.add('bi-box-arrow-in-left');
iconRight.classList.remove('bi-box-arrow-in-right');
iconRight.classList.add('bi-box-arrow-in-left');
$('#observations-table').bootstrapTable('resetView');
} else {
resetColumns();
}
});
btnRight.addEventListener('click', () => {
const expanded = btnRight.getAttribute('aria-expanded') === 'true';
if (!expanded) {
colLeft.classList.remove('col-md-6', 'order-2');
colRight.classList.remove('col-md-6', 'order-2');
colRight.classList.add('col-11');
colLeft.classList.add('col-11', 'order-2');
btnRight.setAttribute('aria-expanded', 'true');
btnLeft.setAttribute('aria-expanded', 'false');
iconRight.classList.remove('bi-box-arrow-in-left');
iconRight.classList.add('bi-box-arrow-in-right');
iconLeft.classList.remove('bi-box-arrow-in-left');
iconLeft.classList.add('bi-box-arrow-in-right');
map?.resize();
} else {
resetColumns();
}
});
}
function syncMapWithTable() {
if (!map || !map.getLayer('observations')) {
return;
}
if (!visibleRows.length || visibleRows.length === allObservations.length) {
map.setFilter('observations', null);
return;
}
const ids = visibleRows.map((row) => row.id);
if (!ids.length) {
map.setFilter('observations', ['==', ['get', 'id'], '__none__']);
return;
}
map.setFilter('observations', ['match', ['get', 'id'], ['literal', ids], true, false]);
}
function actionsFormatter(value, row) {
let html = '';
if (row.lon && row.lat) {
html += `
<button class="btn btn-sm btn-light zoom-btn" data-lon="${row.lon}" data-lat="${row.lat}" title="${gettext('Zoomer sur la carte')}">
<i class="bi bi-geo-alt"></i>
</button>`;
}
return html;
}
function codeFormatter(value, row) {
const label = value || '-';
return `<a href="${observationsIndexUrl}${row.id}/" class="fw-semibold text-decoration-none text-nowrap">${label}</a>`;
}
function descriptionFormatter(value, row) {
const fallback = gettext('Observation');
const safeValue = value && value.trim().length ? value.trim() : fallback;
const truncated = truncateText(safeValue, DESCRIPTION_MAX_LENGTH);
// return `<a href="${observationsIndexUrl}${row.id}/" class="fw-semibold text-decoration-none" title="${safeValue}">${truncated}</a>`;
return truncated;
}
function thematicFormatter(value, row) {
if (!value || value === '-') {
return '-';
}
const icon = row.thematic_icon ? `<i class="bi ${row.thematic_icon} me-1"></i>` : '';
return `${icon}${value}`;
}
function statusFormatter(value, row) {
if (!value) {
return '-';
}
const meta = getStatusMetadata(value);
const label = row.status_label || meta.label;
return `<span class="badge text-bg-${row.status_badge || meta.badge}">${label}</span>`;
}
function observationTypeFormatter(value, row) {
const meta = getObservationTypeMetadata(row.observation_type);
const label = value || meta.label;
return `<span class="badge text-bg-${meta.badge}">${label}</span>`;
}
window.actionsFormatter = actionsFormatter;
window.codeFormatter = codeFormatter;
window.descriptionFormatter = descriptionFormatter;
window.thematicFormatter = thematicFormatter;
window.statusFormatter = statusFormatter;
window.observationTypeFormatter = observationTypeFormatter;
// Layout switcher
function setupLayoutButtons() {
const btnTable = document.getElementById('layout-btn-table');
const btnBoth = document.getElementById('layout-btn-both');
const btnMap = document.getElementById('layout-btn-map');
const colLeft = document.getElementById('col-left');
const colRight = document.getElementById('col-right');
if (!btnTable || !btnBoth || !btnMap || !colLeft || !colRight) {
return;
}
function setLayout(layout) {
// Reset button states
[btnTable, btnBoth, btnMap].forEach(btn => btn.classList.remove('text-primary'));
if (layout === 'table_only') {
btnTable.classList.add('text-primary');
colLeft.classList.remove('d-none', 'col-md-6');
colLeft.classList.add('col-12');
colRight.classList.add('d-none');
} else if (layout === 'map_only') {
btnMap.classList.add('text-primary');
colLeft.classList.add('d-none');
colRight.classList.remove('d-none', 'col-md-6');
colRight.classList.add('col-12');
} else {
// both (default)
btnBoth.classList.add('text-primary');
colLeft.classList.remove('d-none', 'col-12');
colLeft.classList.add('col-md-6');
colRight.classList.remove('d-none', 'col-12');
colRight.classList.add('col-md-6');
}
// Save preference in localStorage
localStorage.setItem('observations_layout', layout);
// Resize map and table
setTimeout(() => {
maploader.map?.resize();
$('#observations-table').bootstrapTable('resetView');
}, 100);
}
btnTable.addEventListener('click', () => setLayout('table_only'));
btnBoth.addEventListener('click', () => setLayout('both'));
btnMap.addEventListener('click', () => setLayout('map_only'));
// Restore saved layout
const savedLayout = localStorage.getItem('observations_layout') || 'both';
setLayout(savedLayout);
}
// ==================== Preferences Management ====================
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const OBS_COLUMNS_KEY = 'observations_columns_hidden';
function _saveColumnVisibility($table) {
try {
const allCols = $table.bootstrapTable('getOptions').columns[0] || [];
const visibleFields = $table.bootstrapTable('getVisibleColumns').map((c) => c.field);
const hiddenFields = allCols
.filter((c) => c.field && c.field !== 'actions' && !visibleFields.includes(c.field))
.map((c) => c.field);
localStorage.setItem(OBS_COLUMNS_KEY, JSON.stringify(hiddenFields));
} catch (e) {
// ignore
}
}
function _restoreColumnVisibility($table) {
try {
const raw = localStorage.getItem(OBS_COLUMNS_KEY);
if (!raw) return;
const hiddenFields = JSON.parse(raw);
if (Array.isArray(hiddenFields)) {
hiddenFields.forEach((field) => {
$table.bootstrapTable('hideColumn', field);
});
}
} catch (e) {
// ignore
}
}
function savePreferencesFromUrl() {
if (typeof savePreferencesUrl === 'undefined') return;
const urlParams = new URLSearchParams(window.location.search);
const filters = {};
const filterParams = ['thematic', 'category', 'symptom', 'status', 'type', 'mine', 'q', 'search_mode'];
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];
}
});
const $table = $('#observations-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' };
}
}
fetch(savePreferencesUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') },
body: JSON.stringify({ filters, sort })
}).catch(() => {});
}
async function savePreferencesFromForm(urlParams) {
if (typeof savePreferencesUrl === 'undefined') return;
const filters = {};
const filterParams = ['thematic', 'category', 'symptom', 'status', 'type', 'mine', 'q', 'search_mode'];
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];
}
});
const $table = $('#observations-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' };
}
}
try {
await fetch(savePreferencesUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') },
body: JSON.stringify({ filters, sort }),
keepalive: true
});
} catch (e) {
// Silently fail
}
}
// Save preferences on page load if filters are already present in URL
document.addEventListener('DOMContentLoaded', function () {
if (window.location.search) {
// Debounce to allow table to initialize first
setTimeout(savePreferencesFromUrl, 1000);
}
});