git add loko/assets/static/assets/unified_index.js \
loko/assets/static/assets/unified_map_layers.js \
loko/assets/templates/assets/unified_index.html \
loko/assets/tests_management_zones.py \
loko/assets/views/core.py \
loko/assets/views/management_zones.py
git commit -m "feat(assets): integrate management zones into unified assets view
- Add management zones layers and table row support in unified index
- Replace element type radio filters with 3 checkboxes (assets, locations, management zones)
- Add 3rd card in unified add modal for management zone creation
- Include centroid coordinates, area, and unified properties in management zones GeoJSON
- Add comprehensive integration test suite"
This commit is contained in:
parent
b5b0338e09
commit
3e35c98a46
6 changed files with 438 additions and 56 deletions
|
|
@ -19,7 +19,15 @@ var activeCategories = new Set(SELECTED_CATEGORIES || []);
|
||||||
var activeModels = new Set();
|
var activeModels = new Set();
|
||||||
var modelFilterCollapsed = true;
|
var modelFilterCollapsed = true;
|
||||||
var activeConditionalFilters = {};
|
var activeConditionalFilters = {};
|
||||||
var currentTypeFilter = 'all'; // 'all' | 'locations' | 'assets'
|
var currentTypeFilter = 'all'; // legacy fallback
|
||||||
|
|
||||||
|
function getActiveTypeFilters() {
|
||||||
|
const types = [];
|
||||||
|
if (document.getElementById('type-cb-assets')?.checked) types.push('assets');
|
||||||
|
if (document.getElementById('type-cb-locations')?.checked) types.push('locations');
|
||||||
|
if (document.getElementById('type-cb-management-zones')?.checked) types.push('management_zones');
|
||||||
|
return types;
|
||||||
|
}
|
||||||
var searchTerms = [];
|
var searchTerms = [];
|
||||||
var searchMode = 'and';
|
var searchMode = 'and';
|
||||||
var shouldFitSearch = false;
|
var shouldFitSearch = false;
|
||||||
|
|
@ -658,9 +666,31 @@ function applyInitialPreferences() {
|
||||||
|
|
||||||
// Type filter
|
// Type filter
|
||||||
if (prefs.type_filter) {
|
if (prefs.type_filter) {
|
||||||
currentTypeFilter = prefs.type_filter;
|
if (Array.isArray(prefs.type_filter)) {
|
||||||
const rb = document.querySelector(`input[name="unified-type-filter"][value="${prefs.type_filter}"]`);
|
const cbA = document.getElementById('type-cb-assets');
|
||||||
if (rb) rb.checked = true;
|
const cbL = document.getElementById('type-cb-locations');
|
||||||
|
const cbZ = document.getElementById('type-cb-management-zones');
|
||||||
|
if (cbA) cbA.checked = prefs.type_filter.includes('assets');
|
||||||
|
if (cbL) cbL.checked = prefs.type_filter.includes('locations');
|
||||||
|
if (cbZ) cbZ.checked = prefs.type_filter.includes('management_zones');
|
||||||
|
} else if (typeof prefs.type_filter === 'string') {
|
||||||
|
const cbA = document.getElementById('type-cb-assets');
|
||||||
|
const cbL = document.getElementById('type-cb-locations');
|
||||||
|
const cbZ = document.getElementById('type-cb-management-zones');
|
||||||
|
if (prefs.type_filter === 'all') {
|
||||||
|
if (cbA) cbA.checked = true;
|
||||||
|
if (cbL) cbL.checked = true;
|
||||||
|
if (cbZ) cbZ.checked = true;
|
||||||
|
} else if (prefs.type_filter === 'locations') {
|
||||||
|
if (cbA) cbA.checked = false;
|
||||||
|
if (cbL) cbL.checked = true;
|
||||||
|
if (cbZ) cbZ.checked = false;
|
||||||
|
} else if (prefs.type_filter === 'assets') {
|
||||||
|
if (cbA) cbA.checked = true;
|
||||||
|
if (cbL) cbL.checked = false;
|
||||||
|
if (cbZ) cbZ.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include no geometry filter
|
// Include no geometry filter
|
||||||
|
|
@ -739,7 +769,7 @@ function savePreferences() {
|
||||||
models: Array.from(activeModels),
|
models: Array.from(activeModels),
|
||||||
conditional_filters: activeConditionalFilters,
|
conditional_filters: activeConditionalFilters,
|
||||||
visible_columns: getVisibleColumnsPreference(),
|
visible_columns: getVisibleColumnsPreference(),
|
||||||
type_filter: currentTypeFilter,
|
type_filter: getActiveTypeFilters(),
|
||||||
status_filters: statusFilters,
|
status_filters: statusFilters,
|
||||||
validation_status_filters: validationStatusFilters,
|
validation_status_filters: validationStatusFilters,
|
||||||
include_no_geom: includeNoGeom,
|
include_no_geom: includeNoGeom,
|
||||||
|
|
@ -784,9 +814,9 @@ $(document).ready(function () {
|
||||||
onUncheckAll: function (rows) { rows.forEach(r => { r.state = false; selectedIds.delete(r.id); }); updateSelectionStatus(); },
|
onUncheckAll: function (rows) { rows.forEach(r => { r.state = false; selectedIds.delete(r.id); }); updateSelectionStatus(); },
|
||||||
onClickRow: function (row) {
|
onClickRow: function (row) {
|
||||||
if (!row.lon || !row.lat) return;
|
if (!row.lon || !row.lat) return;
|
||||||
const source = row.is_location ? 'locations' : 'assets';
|
const source = row.is_management_zone ? 'management_zones' : (row.is_location ? 'locations' : 'assets');
|
||||||
if (typeof highlightMapFeature === 'function') {
|
if (typeof highlightMapFeature === 'function') {
|
||||||
highlightMapFeature(map, source, row.id, row.is_location ? locationshighlightState : assetshighlightState);
|
highlightMapFeature(map, source, row.id, row.is_management_zone ? managementZonesHighlightState : (row.is_location ? locationshighlightState : assetshighlightState));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -865,11 +895,13 @@ $(document).ready(function () {
|
||||||
renderModelFilters();
|
renderModelFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Type radio ────────────────────────────────────────────────────────
|
// ─── Type checkboxes ────────────────────────────────────────────────────
|
||||||
|
|
||||||
document.querySelectorAll('input[name="unified-type-filter"]').forEach(rb => {
|
document.querySelectorAll('input[name="filter-element-type"]').forEach(cb => {
|
||||||
rb.addEventListener('change', function () {
|
cb.addEventListener('change', function () {
|
||||||
if (this.checked) currentTypeFilter = this.value;
|
applyFilters();
|
||||||
|
updateFilterBadge();
|
||||||
|
schedulePreferencesSave();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -884,26 +916,45 @@ $(document).ready(function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
// init from locked element type
|
// init from locked element type
|
||||||
if (PREVENTIVE_ELEMENT_TYPE === 'assets') currentTypeFilter = 'assets';
|
if (PREVENTIVE_ELEMENT_TYPE === 'assets') {
|
||||||
else if (PREVENTIVE_ELEMENT_TYPE === 'locations') currentTypeFilter = 'locations';
|
const cbA = document.getElementById('type-cb-assets');
|
||||||
|
const cbL = document.getElementById('type-cb-locations');
|
||||||
|
const cbZ = document.getElementById('type-cb-management-zones');
|
||||||
|
if (cbA) cbA.checked = true;
|
||||||
|
if (cbL) cbL.checked = false;
|
||||||
|
if (cbZ) cbZ.checked = false;
|
||||||
|
} else if (PREVENTIVE_ELEMENT_TYPE === 'locations') {
|
||||||
|
const cbA = document.getElementById('type-cb-assets');
|
||||||
|
const cbL = document.getElementById('type-cb-locations');
|
||||||
|
const cbZ = document.getElementById('type-cb-management-zones');
|
||||||
|
if (cbA) cbA.checked = false;
|
||||||
|
if (cbL) cbL.checked = true;
|
||||||
|
if (cbZ) cbZ.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Add actions ─────────────────────────────────────────────────────
|
// ─── Add actions ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const addConfig = getUnifiedAddModalConfig();
|
const addConfig = getUnifiedAddModalConfig();
|
||||||
const assetThematics = Array.isArray(addConfig.asset_thematics) ? addConfig.asset_thematics : [];
|
const assetThematics = Array.isArray(addConfig.asset_thematics) ? addConfig.asset_thematics : [];
|
||||||
const locationThematics = Array.isArray(addConfig.location_thematics) ? addConfig.location_thematics : [];
|
const locationThematics = Array.isArray(addConfig.location_thematics) ? addConfig.location_thematics : [];
|
||||||
|
const managementZoneThematics = Array.isArray(addConfig.management_zone_thematics) ? addConfig.management_zone_thematics : [];
|
||||||
const addButton = document.getElementById('btn-add-asset');
|
const addButton = document.getElementById('btn-add-asset');
|
||||||
const assetThematicSelect = document.getElementById('unified-add-asset-thematic');
|
const assetThematicSelect = document.getElementById('unified-add-asset-thematic');
|
||||||
const locationThematicSelect = document.getElementById('unified-add-location-thematic');
|
const locationThematicSelect = document.getElementById('unified-add-location-thematic');
|
||||||
|
const managementZoneThematicSelect = document.getElementById('unified-add-management-zone-thematic');
|
||||||
const chooserEmpty = document.getElementById('unified-add-chooser-empty');
|
const chooserEmpty = document.getElementById('unified-add-chooser-empty');
|
||||||
|
|
||||||
fillSelectWithOptions(assetThematicSelect, assetThematics, assetThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
fillSelectWithOptions(assetThematicSelect, assetThematics, assetThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
||||||
fillSelectWithOptions(locationThematicSelect, locationThematics, locationThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
fillSelectWithOptions(locationThematicSelect, locationThematics, locationThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
||||||
|
fillSelectWithOptions(managementZoneThematicSelect, managementZoneThematics, managementZoneThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
||||||
|
|
||||||
if (assetThematicSelect) assetThematicSelect.disabled = !assetThematics.length;
|
if (assetThematicSelect) assetThematicSelect.disabled = !assetThematics.length;
|
||||||
if (locationThematicSelect) locationThematicSelect.disabled = !locationThematics.length;
|
if (locationThematicSelect) locationThematicSelect.disabled = !locationThematics.length;
|
||||||
if (chooserEmpty) chooserEmpty.classList.toggle('d-none', assetThematics.length > 0 || locationThematics.length > 0);
|
if (managementZoneThematicSelect) managementZoneThematicSelect.disabled = !managementZoneThematics.length;
|
||||||
if (addButton && !assetThematics.length && !locationThematics.length) {
|
|
||||||
|
const hasAnyAddRights = assetThematics.length > 0 || locationThematics.length > 0 || managementZoneThematics.length > 0;
|
||||||
|
if (chooserEmpty) chooserEmpty.classList.toggle('d-none', hasAnyAddRights);
|
||||||
|
if (addButton && !hasAnyAddRights) {
|
||||||
addButton.disabled = true;
|
addButton.disabled = true;
|
||||||
addButton.title = gettext('Vous n\'avez pas de droits de création');
|
addButton.title = gettext('Vous n\'avez pas de droits de création');
|
||||||
}
|
}
|
||||||
|
|
@ -962,6 +1013,17 @@ $(document).ready(function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('unified-add-management-zone-open')?.addEventListener('click', function () {
|
||||||
|
const thematicCode = managementZoneThematicSelect ? managementZoneThematicSelect.value : '';
|
||||||
|
if (!thematicCode) {
|
||||||
|
setModalAlert('unified-add-chooser-alert', gettext('Sélectionnez une thématique pour créer une zone de gestion.'), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setModalAlert('unified-add-chooser-alert', '', 'info');
|
||||||
|
const baseUrl = addConfig.management_zone_create_url || '/assets/management-zones/add/';
|
||||||
|
window.location.href = `${baseUrl}?thematic=${encodeURIComponent(thematicCode)}`;
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('unifiedAddAssetCategory')?.addEventListener('change', function () {
|
document.getElementById('unifiedAddAssetCategory')?.addEventListener('change', function () {
|
||||||
const thematicCode = document.getElementById('unified-add-asset-thematic-code')?.value || '';
|
const thematicCode = document.getElementById('unified-add-asset-thematic-code')?.value || '';
|
||||||
loadUnifiedAssetModels(thematicCode, this.value);
|
loadUnifiedAssetModels(thematicCode, this.value);
|
||||||
|
|
@ -1175,8 +1237,12 @@ $(document).ready(function () {
|
||||||
activeModels.clear();
|
activeModels.clear();
|
||||||
modelFilterCollapsed = true;
|
modelFilterCollapsed = true;
|
||||||
activeConditionalFilters = {};
|
activeConditionalFilters = {};
|
||||||
const allRadio = document.getElementById('type-all');
|
const cbA = document.getElementById('type-cb-assets');
|
||||||
if (allRadio) { allRadio.checked = true; currentTypeFilter = 'all'; }
|
const cbL = document.getElementById('type-cb-locations');
|
||||||
|
const cbZ = document.getElementById('type-cb-management-zones');
|
||||||
|
if (cbA) cbA.checked = true;
|
||||||
|
if (cbL) cbL.checked = true;
|
||||||
|
if (cbZ) cbZ.checked = true;
|
||||||
document.querySelectorAll('[name="filter-status"]').forEach(cb => { cb.checked = false; });
|
document.querySelectorAll('[name="filter-status"]').forEach(cb => { cb.checked = false; });
|
||||||
document.querySelectorAll('[name="filter-validation-status"]').forEach(cb => { cb.checked = false; });
|
document.querySelectorAll('[name="filter-validation-status"]').forEach(cb => { cb.checked = false; });
|
||||||
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
||||||
|
|
@ -1860,11 +1926,13 @@ function findUnifiedRowIndex(rowRef) {
|
||||||
const rows = $table.bootstrapTable('getData') || [];
|
const rows = $table.bootstrapTable('getData') || [];
|
||||||
|
|
||||||
const isLocation = !!rowRef.is_location;
|
const isLocation = !!rowRef.is_location;
|
||||||
|
const isManagementZone = !!rowRef.is_management_zone;
|
||||||
const matches = (row) => {
|
const matches = (row) => {
|
||||||
if (!!row.is_location !== isLocation) return false;
|
if (!!row.is_location !== isLocation) return false;
|
||||||
|
if (!!row.is_management_zone !== isManagementZone) return false;
|
||||||
if (String(row.id) !== String(rowRef.id)) return false;
|
if (String(row.id) !== String(rowRef.id)) return false;
|
||||||
|
|
||||||
if (!isLocation) {
|
if (!isLocation && !isManagementZone) {
|
||||||
if (String(row.thematic || '') !== String(rowRef.thematic || '')) return false;
|
if (String(row.thematic || '') !== String(rowRef.thematic || '')) return false;
|
||||||
if (String(row.asset_type || '') !== String(rowRef.asset_type || '')) return false;
|
if (String(row.asset_type || '') !== String(rowRef.asset_type || '')) return false;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1878,7 +1946,7 @@ function findUnifiedRowIndex(rowRef) {
|
||||||
if (idx !== -1) return idx;
|
if (idx !== -1) return idx;
|
||||||
|
|
||||||
// Fallback in case some rows do not expose all discriminators.
|
// Fallback in case some rows do not expose all discriminators.
|
||||||
idx = rows.findIndex(row => !!row.is_location === isLocation && String(row.id) === String(rowRef.id));
|
idx = rows.findIndex(row => !!row.is_location === isLocation && !!row.is_management_zone === isManagementZone && String(row.id) === String(rowRef.id));
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1953,6 +2021,37 @@ function collectClickedAssets(clickEvent) {
|
||||||
return Array.from(unique.values());
|
return Array.from(unique.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectClickedManagementZones(clickEvent) {
|
||||||
|
const layers = ['management-zones-fill', 'management-zones-line'].filter(layerId => map.getLayer(layerId));
|
||||||
|
if (!layers.length) return [];
|
||||||
|
|
||||||
|
const rendered = map.queryRenderedFeatures(clickEvent.point, { layers });
|
||||||
|
const unique = new Map();
|
||||||
|
|
||||||
|
rendered.forEach(f => {
|
||||||
|
const p = f.properties || {};
|
||||||
|
const id = f.id ?? p.id;
|
||||||
|
if (id == null) return;
|
||||||
|
const key = `zone|${p.thematic || ''}|${id}`;
|
||||||
|
if (unique.has(key)) return;
|
||||||
|
unique.set(key, {
|
||||||
|
id: id,
|
||||||
|
code: p.code || '',
|
||||||
|
name: p.name || '',
|
||||||
|
thematic: p.thematic || '',
|
||||||
|
thematic_name: f._thematic_name || p.thematic_name || p.thematic || '',
|
||||||
|
element_type: p.element_type || gettext('Zone de gestion'),
|
||||||
|
category: p.category || p.zone_type_name || '',
|
||||||
|
detail_url: p.detail_url || null,
|
||||||
|
color: p.color || '',
|
||||||
|
feature_id: id,
|
||||||
|
is_management_zone: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(unique.values());
|
||||||
|
}
|
||||||
|
|
||||||
function collectClickedLocations(clickEvent) {
|
function collectClickedLocations(clickEvent) {
|
||||||
const layers = ['locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline'].filter(layerId => map.getLayer(layerId));
|
const layers = ['locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline'].filter(layerId => map.getLayer(layerId));
|
||||||
if (!layers.length) return [];
|
if (!layers.length) return [];
|
||||||
|
|
@ -2016,12 +2115,29 @@ function bindAssetPopupActions(popupNode) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
popupNode.querySelectorAll('.zone-popup-focus-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const rowRef = {
|
||||||
|
id: this.dataset.zoneId,
|
||||||
|
thematic: this.dataset.thematic,
|
||||||
|
is_management_zone: true,
|
||||||
|
};
|
||||||
|
focusRowInUnifiedTable(rowRef);
|
||||||
|
|
||||||
|
if (typeof highlightMapFeature === 'function') {
|
||||||
|
highlightMapFeature(map, 'management_zones', rowRef.id, managementZonesHighlightState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAssetClickPopup(clickEvent) {
|
function showAssetClickPopup(clickEvent) {
|
||||||
const assets = collectClickedAssets(clickEvent);
|
const assets = collectClickedAssets(clickEvent);
|
||||||
const locations = collectClickedLocations(clickEvent);
|
const locations = collectClickedLocations(clickEvent);
|
||||||
if (!assets.length && !locations.length) return;
|
const managementZones = collectClickedManagementZones(clickEvent);
|
||||||
|
if (!assets.length && !locations.length && !managementZones.length) return;
|
||||||
|
|
||||||
const maxItems = 8;
|
const maxItems = 8;
|
||||||
const visibleAssets = assets.slice(0, maxItems);
|
const visibleAssets = assets.slice(0, maxItems);
|
||||||
|
|
@ -2113,10 +2229,56 @@ function showAssetClickPopup(clickEvent) {
|
||||||
`
|
`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const visibleZones = managementZones.slice(0, maxItems);
|
||||||
|
const hiddenZonesCount = Math.max(0, managementZones.length - visibleZones.length);
|
||||||
|
|
||||||
|
const zoneItemsHtml = visibleZones.map(z => {
|
||||||
|
const code = escapeHtml(z.code || '-');
|
||||||
|
const type = escapeHtml(z.element_type || gettext('Zone de gestion'));
|
||||||
|
const thematic = escapeHtml(z.thematic_name || z.thematic || '');
|
||||||
|
const cat = z.category ? ` • ${escapeHtml(z.category)}` : '';
|
||||||
|
const dotHtml = z.color ? `<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background-color:${escapeHtml(z.color)}; margin-right:4px; vertical-align:middle; box-shadow:0 0 0 1px rgba(0,0,0,0.15);"></span>` : '';
|
||||||
|
const name = escapeHtml(z.name || '');
|
||||||
|
const detailAnchor = z.detail_url
|
||||||
|
? `<a href="${escapeHtml(z.detail_url)}">${code}</a>`
|
||||||
|
: `<span>${code}</span>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="zone-popup-item" style="display:flex; gap:0.5rem; align-items:flex-start; justify-content:space-between; padding:0.25rem 0; border-bottom:1px solid #f1f3f5;">
|
||||||
|
<div style="min-width:0;">
|
||||||
|
<div style="font-weight:600; line-height:1.1;">${detailAnchor}</div>
|
||||||
|
<div style="font-size:0.75rem; color:#6c757d; line-height:1.2;">${dotHtml}${type}${cat}${thematic ? ' - ' + thematic : ''}</div>
|
||||||
|
${name ? `<div style="font-size:0.75rem; color:#495057; line-height:1.2;">${name}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-light zone-popup-focus-btn"
|
||||||
|
data-zone-id="${escapeHtml(z.id)}"
|
||||||
|
data-thematic="${escapeHtml(z.thematic)}"
|
||||||
|
title="${escapeHtml(gettext('Voir dans le tableau'))}"
|
||||||
|
style="transform:translateX(-5px);">
|
||||||
|
<i class="bi bi-table"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const moreZonesHtml = hiddenZonesCount > 0
|
||||||
|
? `<div style="margin-top:0.35rem; font-size:0.75rem; color:#6c757d;">+${hiddenZonesCount} ${escapeHtml(gettext('autre(s) zone(s)'))}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const managementZonesSectionHtml = managementZones.length > 0
|
||||||
|
? `
|
||||||
|
<div style="font-weight:700; margin:${(assets.length || locations.length) ? '0.6rem' : '0'} 0 0.35rem 0;">${escapeHtml(gettext('Zones de gestion trouvées'))}: ${managementZones.length}</div>
|
||||||
|
<div>${zoneItemsHtml}</div>
|
||||||
|
${moreZonesHtml}
|
||||||
|
`
|
||||||
|
: '';
|
||||||
|
|
||||||
const popupHtml = `
|
const popupHtml = `
|
||||||
<div style="min-width:260px; max-width:320px;">
|
<div style="min-width:260px; max-width:320px;">
|
||||||
${assetsSectionHtml}
|
${assetsSectionHtml}
|
||||||
${locationsSectionHtml}
|
${locationsSectionHtml}
|
||||||
|
${managementZonesSectionHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -2307,6 +2469,23 @@ function refreshData() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Management zones: loaded per active thematic
|
||||||
|
if (cfg.management_zones_geojson_url) {
|
||||||
|
if (!('management_zones' in geojsonCache[code])) {
|
||||||
|
const url = cfg.management_zones_geojson_url + '?thematic=' + encodeURIComponent(code);
|
||||||
|
fetches.push(
|
||||||
|
fetchGeojsonFeatures(url, { signal }, signal)
|
||||||
|
.then(data => {
|
||||||
|
geojsonCache[code].management_zones = data.features || [];
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.name === 'AbortError') throw err;
|
||||||
|
geojsonCache[code].management_zones = [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Assets: conditional loading
|
// ── Assets: conditional loading
|
||||||
const alwaysLoad = ALWAYS_LOAD_ASSETS_THEMATICS.has(code);
|
const alwaysLoad = ALWAYS_LOAD_ASSETS_THEMATICS.has(code);
|
||||||
const prevMode = assetsLoadModes[code] || null;
|
const prevMode = assetsLoadModes[code] || null;
|
||||||
|
|
@ -2382,6 +2561,10 @@ function refreshData() {
|
||||||
f._thematic_name = thematicName;
|
f._thematic_name = thematicName;
|
||||||
allFeatures.push(f);
|
allFeatures.push(f);
|
||||||
});
|
});
|
||||||
|
(cache.management_zones || []).forEach(f => {
|
||||||
|
f._thematic_name = thematicName;
|
||||||
|
allFeatures.push(f);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
renderModelFilters();
|
renderModelFilters();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
|
|
@ -2406,19 +2589,27 @@ function applyFilters() {
|
||||||
let features = allFeatures.slice();
|
let features = allFeatures.slice();
|
||||||
|
|
||||||
// Type filter
|
// Type filter
|
||||||
if (currentTypeFilter === 'locations') {
|
const cbAssets = document.getElementById('type-cb-assets');
|
||||||
features = features.filter(f => f.properties && f.properties.is_location);
|
const cbLocations = document.getElementById('type-cb-locations');
|
||||||
} else if (currentTypeFilter === 'assets') {
|
const cbZones = document.getElementById('type-cb-management-zones');
|
||||||
features = features.filter(f => f.properties && !f.properties.is_location);
|
const showAssets = cbAssets ? cbAssets.checked : true;
|
||||||
}
|
const showLocations = cbLocations ? cbLocations.checked : true;
|
||||||
|
const showManagementZones = cbZones ? cbZones.checked : true;
|
||||||
|
|
||||||
|
features = features.filter(f => {
|
||||||
|
if (!f.properties) return false;
|
||||||
|
if (f.properties.is_management_zone) return showManagementZones;
|
||||||
|
if (f.properties.is_location) return showLocations;
|
||||||
|
return showAssets;
|
||||||
|
});
|
||||||
|
|
||||||
// Include no geom filter
|
// Include no geom filter
|
||||||
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
||||||
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
|
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
|
||||||
if (!includeNoGeom) {
|
if (!includeNoGeom) {
|
||||||
features = features.filter(f => {
|
features = features.filter(f => {
|
||||||
// Locations are never filtered by geometry checkbox (only assets)
|
// Locations and management zones are never filtered by geometry checkbox (only assets)
|
||||||
if (f.properties && f.properties.is_location) return true;
|
if (f.properties && (f.properties.is_location || f.properties.is_management_zone)) return true;
|
||||||
return f.geometry && (f.geometry.coordinates || f.geometry.type);
|
return f.geometry && (f.geometry.coordinates || f.geometry.type);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2435,7 +2626,7 @@ function applyFilters() {
|
||||||
const selectedValidationStatuses = Array.from(document.querySelectorAll('[name="filter-validation-status"]:checked')).map(cb => cb.value);
|
const selectedValidationStatuses = Array.from(document.querySelectorAll('[name="filter-validation-status"]:checked')).map(cb => cb.value);
|
||||||
if (selectedValidationStatuses.length) {
|
if (selectedValidationStatuses.length) {
|
||||||
features = features.filter(f => {
|
features = features.filter(f => {
|
||||||
if (f.properties && f.properties.is_location) return true;
|
if (f.properties && (f.properties.is_location || f.properties.is_management_zone)) return true;
|
||||||
return selectedValidationStatuses.includes(f.properties && f.properties.validation_status);
|
return selectedValidationStatuses.includes(f.properties && f.properties.validation_status);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2443,7 +2634,7 @@ function applyFilters() {
|
||||||
// Category filter
|
// Category filter
|
||||||
if (activeCategories.size) {
|
if (activeCategories.size) {
|
||||||
features = features.filter(f => {
|
features = features.filter(f => {
|
||||||
if (f.properties && f.properties.is_location) return true;
|
if (f.properties && (f.properties.is_location || f.properties.is_management_zone)) return true;
|
||||||
return activeCategories.has(getFeatureCategoryValue(f));
|
return activeCategories.has(getFeatureCategoryValue(f));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2451,7 +2642,7 @@ function applyFilters() {
|
||||||
// Model filter
|
// Model filter
|
||||||
if (activeModels.size) {
|
if (activeModels.size) {
|
||||||
features = features.filter(f => {
|
features = features.filter(f => {
|
||||||
if (f.properties && f.properties.is_location) return true;
|
if (f.properties && (f.properties.is_location || f.properties.is_management_zone)) return true;
|
||||||
return activeModels.has(getFeatureModelValue(f));
|
return activeModels.has(getFeatureModelValue(f));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2519,6 +2710,7 @@ function loadTableData(features) {
|
||||||
length: Number.isFinite(length) ? length : 0,
|
length: Number.isFinite(length) ? length : 0,
|
||||||
area: Number.isFinite(area) ? area : 0,
|
area: Number.isFinite(area) ? area : 0,
|
||||||
is_location: p.is_location,
|
is_location: p.is_location,
|
||||||
|
is_management_zone: p.is_management_zone || false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2539,6 +2731,7 @@ function loadTableData(features) {
|
||||||
|
|
||||||
var locationshighlightState = { previousId: null };
|
var locationshighlightState = { previousId: null };
|
||||||
var assetshighlightState = { previousId: null };
|
var assetshighlightState = { previousId: null };
|
||||||
|
var managementZonesHighlightState = { previousId: null };
|
||||||
|
|
||||||
|
|
||||||
function isPanelTextSpriteId(missingId) {
|
function isPanelTextSpriteId(missingId) {
|
||||||
|
|
@ -2611,15 +2804,20 @@ function rebuildMapSources() {
|
||||||
if (typeof map === 'undefined') return;
|
if (typeof map === 'undefined') return;
|
||||||
const locSrc = map.getSource('locations');
|
const locSrc = map.getSource('locations');
|
||||||
const asstSrc = map.getSource('assets');
|
const asstSrc = map.getSource('assets');
|
||||||
|
const zoneSrc = map.getSource('management_zones');
|
||||||
if (!locSrc || !asstSrc) return; // sources not yet added (before mapLoaded)
|
if (!locSrc || !asstSrc) return; // sources not yet added (before mapLoaded)
|
||||||
|
|
||||||
mergedLocationsGeojson = {
|
mergedLocationsGeojson = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && f.properties.is_location),
|
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && f.properties.is_location),
|
||||||
};
|
};
|
||||||
|
mergedManagementZonesGeojson = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && f.properties.is_management_zone),
|
||||||
|
};
|
||||||
mergedAssetsGeojson = {
|
mergedAssetsGeojson = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && !f.properties.is_location),
|
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && !f.properties.is_location && !f.properties.is_management_zone),
|
||||||
};
|
};
|
||||||
|
|
||||||
applyUnifiedSignPanelOffsets(mergedAssetsGeojson);
|
applyUnifiedSignPanelOffsets(mergedAssetsGeojson);
|
||||||
|
|
@ -2627,6 +2825,7 @@ function rebuildMapSources() {
|
||||||
|
|
||||||
locSrc.setData(mergedLocationsGeojson);
|
locSrc.setData(mergedLocationsGeojson);
|
||||||
asstSrc.setData(mergedAssetsGeojson);
|
asstSrc.setData(mergedAssetsGeojson);
|
||||||
|
if (zoneSrc) zoneSrc.setData(mergedManagementZonesGeojson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2642,6 +2841,7 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
data: window.TERRITORY_MASK_URL || '/common/territory-mask-geojson/'
|
data: window.TERRITORY_MASK_URL || '/common/territory-mask-geojson/'
|
||||||
});
|
});
|
||||||
map.addSource('locations', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
map.addSource('locations', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||||
|
map.addSource('management_zones', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||||
map.addSource('assets', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
map.addSource('assets', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||||
|
|
||||||
// Add layers in render order: fills → lines → circles/symbols (see UNIFIED_LAYERS)
|
// Add layers in render order: fills → lines → circles/symbols (see UNIFIED_LAYERS)
|
||||||
|
|
@ -2782,10 +2982,10 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click interaction on map elements: show popup when assets are found at click position.
|
// Click interaction on map elements: show popup when assets are found at click position.
|
||||||
['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline'].forEach(layerId => {
|
['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline', 'management-zones-fill', 'management-zones-line'].forEach(layerId => {
|
||||||
if (!map.getLayer(layerId)) return;
|
if (!map.getLayer(layerId)) return;
|
||||||
map.on('click', layerId, showAssetClickPopup);
|
map.on('click', layerId, showAssetClickPopup);
|
||||||
if (['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical'].includes(layerId)) {
|
if (['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'management-zones-fill'].includes(layerId)) {
|
||||||
map.on('mouseenter', layerId, function () { map.getCanvas().style.cursor = 'pointer'; });
|
map.on('mouseenter', layerId, function () { map.getCanvas().style.cursor = 'pointer'; });
|
||||||
map.on('mouseleave', layerId, function () { map.getCanvas().style.cursor = ''; });
|
map.on('mouseleave', layerId, function () { map.getCanvas().style.cursor = ''; });
|
||||||
}
|
}
|
||||||
|
|
@ -2795,7 +2995,7 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
map.on('click', function (e) {
|
map.on('click', function (e) {
|
||||||
if (!assetClickPopup || !assetClickPopup.isOpen()) return;
|
if (!assetClickPopup || !assetClickPopup.isOpen()) return;
|
||||||
|
|
||||||
const interactiveLayers = ['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline']
|
const interactiveLayers = ['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline', 'management-zones-fill', 'management-zones-line']
|
||||||
.filter(layerId => map.getLayer(layerId));
|
.filter(layerId => map.getLayer(layerId));
|
||||||
if (!interactiveLayers.length) {
|
if (!interactiveLayers.length) {
|
||||||
assetClickPopup.remove();
|
assetClickPopup.remove();
|
||||||
|
|
@ -2856,7 +3056,7 @@ function updateFilterBadge() {
|
||||||
}, 0);
|
}, 0);
|
||||||
n += thematicBlocksCount;
|
n += thematicBlocksCount;
|
||||||
|
|
||||||
if (currentTypeFilter !== 'all') n += 1;
|
if (getActiveTypeFilters().length < 3) n += 1;
|
||||||
if (document.querySelectorAll('[name="filter-status"]:checked').length > 0) n += 1;
|
if (document.querySelectorAll('[name="filter-status"]:checked').length > 0) n += 1;
|
||||||
if (document.querySelectorAll('[name="filter-validation-status"]:checked').length > 0) n += 1;
|
if (document.querySelectorAll('[name="filter-validation-status"]:checked').length > 0) n += 1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,34 @@ const _unified_locations_fill = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Layer 1b — management zones fill ─────────────────────────────────────────
|
||||||
|
const _unified_management_zones_fill = {
|
||||||
|
id: 'management-zones-fill',
|
||||||
|
type: 'fill',
|
||||||
|
source: 'management_zones',
|
||||||
|
filter: ['any',
|
||||||
|
['==', ['geometry-type'], 'Polygon'],
|
||||||
|
['==', ['geometry-type'], 'MultiPolygon']
|
||||||
|
],
|
||||||
|
paint: {
|
||||||
|
'fill-color': ['coalesce', ['get', 'color'], '#10b981'],
|
||||||
|
'fill-opacity': 0.25,
|
||||||
|
'fill-antialias': false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Layer 3b — management zones line ─────────────────────────────────────────
|
||||||
|
const _unified_management_zones_line = {
|
||||||
|
id: 'management-zones-line',
|
||||||
|
type: 'line',
|
||||||
|
source: 'management_zones',
|
||||||
|
paint: {
|
||||||
|
'line-color': ['coalesce', ['get', 'color'], '#10b981'],
|
||||||
|
'line-width': 2,
|
||||||
|
'line-dasharray': [3, 2],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Layer 2 — asset fill (polygon assets: road surfaces, nature surfaces) ────
|
// ─── Layer 2 — asset fill (polygon assets: road surfaces, nature surfaces) ────
|
||||||
const _unified_assets_fill = (typeof AssetSymbology !== 'undefined' && AssetSymbology.getAssetsFillLayer)
|
const _unified_assets_fill = (typeof AssetSymbology !== 'undefined' && AssetSymbology.getAssetsFillLayer)
|
||||||
? AssetSymbology.getAssetsFillLayer('assets')
|
? AssetSymbology.getAssetsFillLayer('assets')
|
||||||
|
|
@ -1145,10 +1173,12 @@ const UNIFIED_LAYERS = [
|
||||||
_unified_territory_mask, // 0. territory mask (outside commune)
|
_unified_territory_mask, // 0. territory mask (outside commune)
|
||||||
_unified_territory_outline, // 0b. territory boundary line
|
_unified_territory_outline, // 0b. territory boundary line
|
||||||
_unified_locations_fill, // 1. polygon fills (bottom)
|
_unified_locations_fill, // 1. polygon fills (bottom)
|
||||||
|
_unified_management_zones_fill, // 1b. management zones fill
|
||||||
_unified_assets_fill, // 2. polygon fills
|
_unified_assets_fill, // 2. polygon fills
|
||||||
_unified_locations_outline, // 3. lines
|
_unified_locations_outline, // 3. lines
|
||||||
_unified_assets_line, // 3b. asset lines
|
_unified_management_zones_line, // 3b. management zones line
|
||||||
_unified_assets_polygon_outline, // 3c. polygon asset outlines
|
_unified_assets_line, // 3c. asset lines
|
||||||
|
_unified_assets_polygon_outline, // 3d. polygon asset outlines
|
||||||
_unified_locations_circle, // 4. circles
|
_unified_locations_circle, // 4. circles
|
||||||
_unified_locations_radar_outline,
|
_unified_locations_radar_outline,
|
||||||
_unified_locations_radar,
|
_unified_locations_radar,
|
||||||
|
|
|
||||||
|
|
@ -670,24 +670,24 @@
|
||||||
<div class="filter-section-header">
|
<div class="filter-section-header">
|
||||||
<label>{% translate "Type d'élément" %}</label>
|
<label>{% translate "Type d'élément" %}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="segmented-control">
|
<div class="filter-options d-flex gap-3 flex-wrap">
|
||||||
<div class="filter-option segmented-option">
|
<div class="filter-option">
|
||||||
<input type="radio" name="unified-type-filter" id="type-all" value="all"
|
<input type="checkbox" name="filter-element-type" id="type-cb-assets" value="assets"
|
||||||
{% if preventive_element_type != 'assets' and preventive_element_type != 'locations' %}checked{% endif %}
|
{% if preventive_element_type != 'locations' %}checked{% endif %}
|
||||||
{% if preventive_element_type %}disabled{% endif %}>
|
{% if preventive_element_type == 'locations' %}disabled{% endif %}>
|
||||||
<label for="type-all">{% translate "Assets & localisations" %}</label>
|
<label for="type-cb-assets"><i class="bi bi-layers me-1 text-primary"></i>{% translate "Assets" %}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-option segmented-option">
|
<div class="filter-option">
|
||||||
<input type="radio" name="unified-type-filter" id="type-locations" value="locations"
|
<input type="checkbox" name="filter-element-type" id="type-cb-locations" value="locations"
|
||||||
{% if preventive_element_type == 'locations' %}checked{% endif %}
|
{% if preventive_element_type != 'assets' %}checked{% endif %}
|
||||||
{% if preventive_element_type %}disabled{% endif %}>
|
{% if preventive_element_type == 'assets' %}disabled{% endif %}>
|
||||||
<label for="type-locations">{% translate "Localisations uniquement" %}</label>
|
<label for="type-cb-locations"><i class="bi bi-geo-alt me-1 text-info"></i>{% translate "Localisations" %}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-option segmented-option">
|
<div class="filter-option">
|
||||||
<input type="radio" name="unified-type-filter" id="type-assets" value="assets"
|
<input type="checkbox" name="filter-element-type" id="type-cb-management-zones" value="management_zones"
|
||||||
{% if preventive_element_type == 'assets' %}checked{% endif %}
|
{% if not preventive_element_type %}checked{% endif %}
|
||||||
{% if preventive_element_type %}disabled{% endif %}>
|
{% if preventive_element_type %}disabled{% endif %}>
|
||||||
<label for="type-assets">{% translate "Assets uniquement" %}</label>
|
<label for="type-cb-management-zones"><i class="bi bi-diagram-3 me-1 text-success"></i>{% translate "Zones de gestion" %}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -837,7 +837,7 @@
|
||||||
{% translate "Choisissez d'abord le type d'élément, puis la thématique autorisée." %}
|
{% translate "Choisissez d'abord le type d'élément, puis la thématique autorisée." %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="unified-add-panel h-100">
|
<div class="unified-add-panel h-100">
|
||||||
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-layers"></i>{% translate "Asset" %}</h6>
|
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-layers"></i>{% translate "Asset" %}</h6>
|
||||||
<label for="unified-add-asset-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
<label for="unified-add-asset-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
||||||
|
|
@ -847,7 +847,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="unified-add-panel h-100">
|
<div class="unified-add-panel h-100">
|
||||||
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-geo-alt"></i>{% translate "Localisation" %}</h6>
|
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-geo-alt"></i>{% translate "Localisation" %}</h6>
|
||||||
<label for="unified-add-location-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
<label for="unified-add-location-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
||||||
|
|
@ -857,6 +857,16 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="unified-add-panel h-100">
|
||||||
|
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-diagram-3 text-success"></i>{% translate "Zone de gestion" %}</h6>
|
||||||
|
<label for="unified-add-management-zone-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
||||||
|
<select id="unified-add-management-zone-thematic" class="form-select mb-3"></select>
|
||||||
|
<button type="button" id="unified-add-management-zone-open" class="btn btn-outline-success w-100">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer une zone" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="unified-add-chooser-empty" class="text-muted small mt-3 d-none">
|
<div id="unified-add-chooser-empty" class="text-muted small mt-3 d-none">
|
||||||
{% translate "Aucune thématique éditable n'est disponible pour la création." %}
|
{% translate "Aucune thématique éditable n'est disponible pour la création." %}
|
||||||
|
|
|
||||||
|
|
@ -486,3 +486,108 @@ class ManagementZonesPermissionAndCRUDTests(TestCase):
|
||||||
data = res.json()
|
data = res.json()
|
||||||
self.assertEqual(len(data['types']), 1)
|
self.assertEqual(len(data['types']), 1)
|
||||||
self.assertEqual(data['types'][0]['code'], 'fauche_eco')
|
self.assertEqual(data['types'][0]['code'], 'fauche_eco')
|
||||||
|
|
||||||
|
|
||||||
|
class ManagementZoneUnifiedViewIntegrationTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.thematic_nature = Thematic.objects.create(name_fr="Nature", name_nl="Natuur", code="nature")
|
||||||
|
self.thematic_roads = Thematic.objects.create(name_fr="Voirie", name_nl="Wegen", code="roads")
|
||||||
|
|
||||||
|
self.user_editor = User.objects.create_user(username="editor_unified", password="password")
|
||||||
|
self.user_viewer = User.objects.create_user(username="viewer_unified", password="password")
|
||||||
|
|
||||||
|
self.config_editor = UserConfig.objects.create(user=self.user_editor)
|
||||||
|
self.config_viewer = UserConfig.objects.create(user=self.user_viewer)
|
||||||
|
|
||||||
|
UserThematics.objects.create(
|
||||||
|
user_config=self.config_editor,
|
||||||
|
thematic=self.thematic_nature,
|
||||||
|
can_view_assets=True,
|
||||||
|
can_edit_assets=True,
|
||||||
|
can_edit_management_zones=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
UserThematics.objects.create(
|
||||||
|
user_config=self.config_viewer,
|
||||||
|
thematic=self.thematic_nature,
|
||||||
|
can_view_assets=True,
|
||||||
|
can_edit_assets=False,
|
||||||
|
can_edit_management_zones=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.zone_type = ManagementZoneType.objects.create(
|
||||||
|
thematic=self.thematic_nature,
|
||||||
|
code="fauche_eco",
|
||||||
|
name_fr="Fauche tardive",
|
||||||
|
name_nl="Laat maaien",
|
||||||
|
default_color="#27ae60",
|
||||||
|
)
|
||||||
|
|
||||||
|
poly = Polygon(((149000, 170000), (149100, 170000), (149100, 170100), (149000, 170100), (149000, 170000)))
|
||||||
|
multi_poly = MultiPolygon([poly], srid=3812)
|
||||||
|
self.zone = ManagementZone.objects.create(
|
||||||
|
thematic=self.thematic_nature,
|
||||||
|
zone_type=self.zone_type,
|
||||||
|
code="ZN-TEST-001",
|
||||||
|
name_fr="Pelouse A",
|
||||||
|
name_nl="Grasveld A",
|
||||||
|
geom=multi_poly,
|
||||||
|
is_active=True,
|
||||||
|
created_by=self.user_editor,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_build_thematic_config_includes_management_zones_url(self):
|
||||||
|
from assets.views.core import build_thematic_config
|
||||||
|
config = build_thematic_config([self.thematic_nature, self.thematic_roads])
|
||||||
|
self.assertIn("nature", config)
|
||||||
|
self.assertIn("management_zones_geojson_url", config["nature"])
|
||||||
|
self.assertEqual(config["nature"]["management_zones_geojson_url"], reverse("assets:management_zones_geojson"))
|
||||||
|
|
||||||
|
def test_build_unified_add_modal_config_permissions(self):
|
||||||
|
from assets.views.core import build_unified_add_modal_config
|
||||||
|
# Editor can edit management zones in nature
|
||||||
|
editor_config = build_unified_add_modal_config([self.thematic_nature], user=self.user_editor)
|
||||||
|
self.assertIn("management_zone_thematics", editor_config)
|
||||||
|
editor_codes = [t["code"] for t in editor_config["management_zone_thematics"]]
|
||||||
|
self.assertIn("nature", editor_codes)
|
||||||
|
|
||||||
|
# Viewer cannot edit management zones
|
||||||
|
viewer_config = build_unified_add_modal_config([self.thematic_nature], user=self.user_viewer)
|
||||||
|
viewer_codes = [t["code"] for t in viewer_config["management_zone_thematics"]]
|
||||||
|
self.assertNotIn("nature", viewer_codes)
|
||||||
|
|
||||||
|
def test_management_zones_geojson_properties_for_unified_table(self):
|
||||||
|
self.client.login(username="editor_unified", password="password")
|
||||||
|
url = reverse("assets:management_zones_geojson") + f"?thematic={self.thematic_nature.code}"
|
||||||
|
res = self.client.get(url)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
data = res.json()
|
||||||
|
self.assertEqual(len(data["features"]), 1)
|
||||||
|
feat = data["features"][0]
|
||||||
|
props = feat["properties"]
|
||||||
|
|
||||||
|
self.assertTrue(props["is_management_zone"])
|
||||||
|
self.assertFalse(props["is_location"])
|
||||||
|
self.assertEqual(props["asset_type"], "managementzone")
|
||||||
|
self.assertIn(props["element_type"], ["Zone de gestion", "Management zone"])
|
||||||
|
self.assertEqual(props["category"], "Fauche tardive")
|
||||||
|
self.assertEqual(props["status"], "active")
|
||||||
|
self.assertIsNotNone(props["lon"])
|
||||||
|
self.assertIsNotNone(props["lat"])
|
||||||
|
self.assertIsNotNone(props["area"])
|
||||||
|
self.assertEqual(props["detail_url"], reverse("assets:management_zone_detail", kwargs={"pk": self.zone.pk}))
|
||||||
|
|
||||||
|
def test_unified_index_page_contains_management_zone_elements(self):
|
||||||
|
self.client.login(username="editor_unified", password="password")
|
||||||
|
url = reverse("assets:index")
|
||||||
|
res = self.client.get(url)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
# Check filter modal checkboxes
|
||||||
|
self.assertContains(res, 'id="type-cb-assets"')
|
||||||
|
self.assertContains(res, 'id="type-cb-locations"')
|
||||||
|
self.assertContains(res, 'id="type-cb-management-zones"')
|
||||||
|
|
||||||
|
# Check add modal card
|
||||||
|
self.assertContains(res, 'id="unified-add-management-zone-thematic"')
|
||||||
|
self.assertContains(res, 'id="unified-add-management-zone-open"')
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,8 @@ ELEMENT_TYPE_LABELS = {
|
||||||
# --- Espaces plein air ---
|
# --- Espaces plein air ---
|
||||||
'outdoorlocation': _('Espace public plein air'),
|
'outdoorlocation': _('Espace public plein air'),
|
||||||
'outdoorasset': _('Équipement plein air'),
|
'outdoorasset': _('Équipement plein air'),
|
||||||
|
# --- Zones de gestion ---
|
||||||
|
'managementzone': _('Zone de gestion'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -553,6 +555,7 @@ def build_thematic_config(thematics):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
management_zones_url = reverse('assets:management_zones_geojson')
|
||||||
result = {}
|
result = {}
|
||||||
for thematic in thematics:
|
for thematic in thematics:
|
||||||
code = thematic.code
|
code = thematic.code
|
||||||
|
|
@ -560,6 +563,7 @@ def build_thematic_config(thematics):
|
||||||
entry = dict(_THEMATIC_URLS[code])
|
entry = dict(_THEMATIC_URLS[code])
|
||||||
entry['name'] = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
|
entry['name'] = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
|
||||||
entry['icon'] = thematic.icon if hasattr(thematic, 'icon') else ''
|
entry['icon'] = thematic.icon if hasattr(thematic, 'icon') else ''
|
||||||
|
entry['management_zones_geojson_url'] = management_zones_url
|
||||||
result[code] = entry
|
result[code] = entry
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -936,6 +940,7 @@ def build_unified_add_modal_config(accessible_thematics, user=None):
|
||||||
from assets.permissions import (
|
from assets.permissions import (
|
||||||
can_edit_location_for_thematic,
|
can_edit_location_for_thematic,
|
||||||
get_allowed_actions_for_thematic,
|
get_allowed_actions_for_thematic,
|
||||||
|
can_edit_management_zones,
|
||||||
)
|
)
|
||||||
|
|
||||||
location_type_by_thematic = {
|
location_type_by_thematic = {
|
||||||
|
|
@ -948,6 +953,7 @@ def build_unified_add_modal_config(accessible_thematics, user=None):
|
||||||
|
|
||||||
asset_thematics = []
|
asset_thematics = []
|
||||||
location_thematics = []
|
location_thematics = []
|
||||||
|
management_zone_thematics = []
|
||||||
|
|
||||||
for thematic in accessible_thematics:
|
for thematic in accessible_thematics:
|
||||||
thematic_name = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
|
thematic_name = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
|
||||||
|
|
@ -974,9 +980,18 @@ def build_unified_add_modal_config(accessible_thematics, user=None):
|
||||||
'location_label': location_meta['label'],
|
'location_label': location_meta['label'],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if can_edit_management_zones(user, thematic):
|
||||||
|
management_zone_thematics.append({
|
||||||
|
'code': thematic.code,
|
||||||
|
'name': thematic_name,
|
||||||
|
'icon': thematic_icon,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'asset_thematics': asset_thematics,
|
'asset_thematics': asset_thematics,
|
||||||
'location_thematics': location_thematics,
|
'location_thematics': location_thematics,
|
||||||
|
'management_zone_thematics': management_zone_thematics,
|
||||||
|
'management_zone_create_url': reverse('assets:management_zone_create'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ def management_zones_geojson(request):
|
||||||
if not thematic:
|
if not thematic:
|
||||||
return JsonResponse({'type': 'FeatureCollection', 'features': []})
|
return JsonResponse({'type': 'FeatureCollection', 'features': []})
|
||||||
|
|
||||||
can_view, _ = get_user_asset_thematic_access(user, thematic)
|
can_view, _can_edit = get_user_asset_thematic_access(user, thematic)
|
||||||
if not can_view:
|
if not can_view:
|
||||||
return HttpResponseForbidden("Access denied to this thematic management zones.")
|
return HttpResponseForbidden("Access denied to this thematic management zones.")
|
||||||
|
|
||||||
|
|
@ -77,6 +77,18 @@ def management_zones_geojson(request):
|
||||||
if not geom_json:
|
if not geom_json:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
centroid = zone.geom.centroid.clone() if zone.geom else None
|
||||||
|
if centroid:
|
||||||
|
if centroid.srid and centroid.srid != 4326:
|
||||||
|
centroid.transform(4326)
|
||||||
|
lon = round(centroid.x, 7)
|
||||||
|
lat = round(centroid.y, 7)
|
||||||
|
else:
|
||||||
|
lon = None
|
||||||
|
lat = None
|
||||||
|
area = round(zone.geom.area, 1) if getattr(zone.geom, 'area', None) is not None else None
|
||||||
|
category_name = zone.zone_type.get_name() if zone.zone_type else ""
|
||||||
|
|
||||||
features.append({
|
features.append({
|
||||||
'type': 'Feature',
|
'type': 'Feature',
|
||||||
'id': zone.id,
|
'id': zone.id,
|
||||||
|
|
@ -87,12 +99,22 @@ def management_zones_geojson(request):
|
||||||
'thematic': zone.thematic.code,
|
'thematic': zone.thematic.code,
|
||||||
'thematic_name': zone.thematic.name,
|
'thematic_name': zone.thematic.name,
|
||||||
'zone_type_code': zone.zone_type.code if zone.zone_type else None,
|
'zone_type_code': zone.zone_type.code if zone.zone_type else None,
|
||||||
'zone_type_name': zone.zone_type.get_name() if zone.zone_type else "",
|
'zone_type_name': category_name,
|
||||||
|
'category': category_name,
|
||||||
'color': zone.get_color(),
|
'color': zone.get_color(),
|
||||||
'instructions': zone.instructions or "",
|
'instructions': zone.instructions or "",
|
||||||
'description': zone.description or "",
|
'description': zone.description or "",
|
||||||
'seasonality_notes': zone.seasonality_notes or "",
|
'seasonality_notes': zone.seasonality_notes or "",
|
||||||
'target_frequency_days': zone.target_frequency_days,
|
'target_frequency_days': zone.target_frequency_days,
|
||||||
|
'status': 'active' if zone.is_active else 'inactive',
|
||||||
|
'element_type': str(_('Zone de gestion')),
|
||||||
|
'asset_type': 'managementzone',
|
||||||
|
'detail_url': reverse('assets:management_zone_detail', kwargs={'pk': zone.pk}),
|
||||||
|
'lon': lon,
|
||||||
|
'lat': lat,
|
||||||
|
'area': area,
|
||||||
|
'is_location': False,
|
||||||
|
'is_management_zone': True,
|
||||||
},
|
},
|
||||||
'geometry': geom_json
|
'geometry': geom_json
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue