loko/streetup/assets/static/assets/inspection_map.js

1248 lines
52 KiB
JavaScript

/**
* Inspection Map JS - Module de carte d'inspection rapide optimisé MapLibre GL, GPS & Mobile
*/
(function() {
let map = null;
let userMarker = null;
let loadedAssets = [];
const STORAGE_THEMATIC_KEY = 'streetup_insp_thematic';
const STORAGE_CATEGORY_KEY = 'streetup_insp_category';
document.addEventListener('DOMContentLoaded', function() {
initMap();
initFilterStorage();
initFormHandler();
setupGeolocation();
});
function initMap() {
const mapContainer = document.getElementById('inspection-map');
if (!mapContainer || typeof maplibregl === 'undefined') return;
if (mapContainer.dataset.mapInitialized === 'true') return;
mapContainer.dataset.mapInitialized = 'true';
// Centrage et style depuis les settings globaux (window.MAP_STYLE_LIGHT / window.DEFAULT_MAP_CENTER)
const defaultCenter = window.DEFAULT_MAP_CENTER || [4.3517, 50.8503];
const defaultZoom = window.DEFAULT_MAP_ZOOM || 15;
const styleUrl = window.MAP_STYLE_LIGHT || 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';
map = new maplibregl.Map({
container: 'inspection-map',
style: styleUrl,
center: defaultCenter,
zoom: defaultZoom,
minzoom: 0,
maxzoom: 24
});
map.addControl(new maplibregl.NavigationControl({ showCompass: true }), 'top-right');
if (typeof LayerSwitcherControl !== 'undefined') {
const layerSwitcher = new LayerSwitcherControl({
layers: [{
id: 'ortho',
name: 'Orthophotos',
icon: 'satellite',
group: 'ortho',
beforeLayer: 'assets-polygons',
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 || '© 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: '© Digitaal Vlaanderen - Orthofoto 2023',
},
},
],
}],
});
map.addControl(layerSwitcher, 'top-right');
}
if (typeof AddressSearchControl !== 'undefined') {
const autocompleteUrl = window.AUTOCOMPLETE_STREET_URL || '/common/autocomplete-street/';
const searchCtrl = new AddressSearchControl({
autocompleteUrl: autocompleteUrl,
placeholder: 'Rechercher une adresse...'
});
map.addControl(searchCtrl, 'top-left');
}
map.on('load', function() {
// Source GeoJSON pour les assets
map.addSource('assets-source', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
// Couche Polygones (ex: ParkingSpot polygon)
map.addLayer({
id: 'assets-polygons',
type: 'fill',
source: 'assets-source',
filter: ['==', '$type', 'Polygon'],
paint: {
'fill-color': '#0d6efd',
'fill-opacity': 0.35,
'fill-outline-color': '#0d6efd'
}
});
// Couche Lignes pour le contour des polygones
map.addLayer({
id: 'assets-lines',
type: 'line',
source: 'assets-source',
filter: ['==', '$type', 'Polygon'],
paint: {
'line-color': '#0d6efd',
'line-width': 2
}
});
// Couche Points (ex: Poteaux, Armoires)
map.addLayer({
id: 'assets-points',
type: 'circle',
source: 'assets-source',
filter: ['==', '$type', 'Point'],
paint: {
'circle-radius': 8,
'circle-color': '#0d6efd',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
// Clic sur un asset
map.on('click', ['assets-polygons', 'assets-points'], function(e) {
// Si un marker d'inspection est présent à cet endroit, ne pas ouvrir la modale directement
if (map.getLayer('inspections-history-points')) {
const inspFeatures = map.queryRenderedFeatures(e.point, { layers: ['inspections-history-points'] });
if (inspFeatures && inspFeatures.length > 0) {
return;
}
}
if (e.features && e.features.length > 0) {
const feature = e.features[0];
handleAssetClick(feature);
}
});
// Survol curseur
map.on('mouseenter', ['assets-polygons', 'assets-points'], function() {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', ['assets-polygons', 'assets-points'], function() {
map.getCanvas().style.cursor = '';
});
// Source GeoJSON pour l'historique des inspections
map.addSource('inspections-history-source', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
// Placer la couche d'inspections EN DESSOUS des couches d'assets
let beforeLayerId = undefined;
if (map.getLayer('assets-polygons')) {
beforeLayerId = 'assets-polygons';
} else if (map.getLayer('assets-points')) {
beforeLayerId = 'assets-points';
}
// Couche Points pour les inspections réalisées sur la carte
map.addLayer({
id: 'inspections-history-points',
type: 'circle',
source: 'inspections-history-source',
paint: {
'circle-radius': 9,
'circle-color': [
'match',
['get', 'latest_result_status'],
'compliant', '#198754',
'non_compliant', '#fd7e14',
'damaged', '#dc3545',
'missing', '#212529',
'updated', '#0dcaf0',
'#6c757d'
],
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
'circle-opacity': 0.85
}
}, beforeLayerId);
// Clic sur un marker d'inspection de la carte
map.on('click', 'inspections-history-points', function(e) {
if (e.originalEvent) {
e.originalEvent.stopPropagation();
if (e.originalEvent.preventDefault) e.originalEvent.preventDefault();
}
if (!e.features || e.features.length === 0) return;
const feat = e.features[0];
let props = feat.properties;
let inspectionsList = [];
if (typeof props.inspections === 'string') {
try { inspectionsList = JSON.parse(props.inspections); } catch(err){}
} else if (Array.isArray(props.inspections)) {
inspectionsList = props.inspections;
}
const targetDisplay = props.target_display || 'Équipement';
const assetProps = {
content_type_id: props.content_type_id,
object_id: props.object_id,
model_name: props.model_name,
spot_type: props.spot_type,
target_display: targetDisplay
};
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
let html = `
<div class="p-1" style="width: 300px; max-width: 310px; font-size: 0.85rem;">
<div class="fw-bold text-dark border-bottom pb-1 mb-2 d-flex align-items-center justify-content-between">
<span class="text-truncate me-2">${escapeHtml(targetDisplay)}</span>
<span class="badge bg-secondary">${props.inspections_count || inspectionsList.length}</span>
</div>
<button type="button" class="btn btn-sm btn-primary w-100 mb-2 py-1.5 fw-semibold d-flex align-items-center justify-content-center gap-1 shadow-sm" id="btn-popup-create-inspection">
<i class="bi bi-plus-circle-fill"></i>
<span>Nouvelle inspection</span>
</button>
<div class="fw-semibold text-muted mb-1 extra-small">
<i class="bi bi-clock-history me-1 text-primary"></i>Inspections (${inspectionsList.length}) :
</div>
<div class="list-group list-group-flush overflow-auto pe-1" style="max-height: 190px;">
`;
inspectionsList.forEach((insp, index) => {
html += `
<div class="list-group-item px-2 py-1.5 border-bottom bg-light rounded mb-1">
<div class="d-flex justify-content-between align-items-center mb-1 gap-2">
<span class="badge ${insp.status_badge_class || 'bg-primary'} extra-small text-truncate flex-shrink-1">${escapeHtml(insp.result_status_display || '')}</span>
<a href="#" class="insp-date-modal-link text-primary text-decoration-underline fw-bold extra-small text-nowrap flex-shrink-0" style="font-size:0.75rem;" data-insp-index="${index}">
<i class="bi bi-calendar-event me-1"></i>${escapeHtml(insp.date || '')}
</a>
</div>
${insp.details_summary ? `<div class="extra-small text-dark mb-1 fw-medium" style="font-size:0.75rem;">${escapeHtml(insp.details_summary)}</div>` : ''}
<div class="d-flex justify-content-between align-items-center extra-small text-muted" style="font-size:0.7rem;">
<span><i class="bi bi-person me-1"></i>${escapeHtml(insp.inspector_name || '-')}</span>
${insp.mobile_asset_url ? `<a href="${insp.mobile_asset_url}" class="text-primary text-decoration-none fw-semibold"><i class="bi bi-box-arrow-up-right me-1"></i>Fiche asset</a>` : ''}
</div>
</div>
`;
});
html += `
</div>
</div>
`;
const popup = new maplibregl.Popup({ closeButton: true, closeOnClick: true })
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map);
setTimeout(function() {
const btnNew = document.getElementById('btn-popup-create-inspection');
if (btnNew) {
btnNew.addEventListener('click', function(ev) {
if (ev) ev.stopPropagation();
popup.remove();
openInspectionModal(assetProps);
});
}
const dateLinks = document.querySelectorAll('.insp-date-modal-link');
dateLinks.forEach(link => {
link.addEventListener('click', function(ev) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
}
const idx = parseInt(this.getAttribute('data-insp-index'), 10);
const inspData = inspectionsList[idx];
if (inspData) {
popup.remove();
showInspectionDetailModal(inspData);
}
});
});
}, 50);
});
map.on('mouseenter', 'inspections-history-points', function() {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'inspections-history-points', function() {
map.getCanvas().style.cursor = '';
});
// Charger les assets et l'historique d'inspections au premier affichage
loadAssets();
loadInspectionsHistory();
setupOverlayResizeObserver();
});
map.on('moveend', onMapMoveEnd);
setTimeout(function() {
if (map) map.resize();
adjustTopControlsMargin();
}, 300);
window.addEventListener('resize', function() {
if (map) map.resize();
adjustTopControlsMargin();
});
}
function adjustTopControlsMargin() {
const overlay = document.querySelector('.map-controls-overlay');
if (!overlay) return;
const overlayHeight = overlay.offsetHeight || 0;
const overlayTop = overlay.offsetTop || 6;
const marginValue = (overlayTop + overlayHeight + 8) + 'px';
const leftCtrl = document.querySelector('#inspection-map .maplibregl-ctrl-top-left');
const rightCtrl = document.querySelector('#inspection-map .maplibregl-ctrl-top-right');
if (leftCtrl) leftCtrl.style.marginTop = marginValue;
if (rightCtrl) rightCtrl.style.marginTop = marginValue;
}
function setupOverlayResizeObserver() {
const overlay = document.querySelector('.map-controls-overlay');
if (!overlay) return;
adjustTopControlsMargin();
if (window.ResizeObserver) {
const observer = new ResizeObserver(function() {
adjustTopControlsMargin();
});
observer.observe(overlay);
} else {
window.addEventListener('resize', adjustTopControlsMargin);
}
}
function getMapBbox() {
if (!map || typeof map.getBounds !== 'function') return null;
const b = map.getBounds();
const sw = b.getSouthWest(), ne = b.getNorthEast();
return {
type: 'Polygon',
coordinates: [[
[sw.lng, sw.lat], [ne.lng, sw.lat],
[ne.lng, ne.lat], [sw.lng, ne.lat],
[sw.lng, sw.lat],
]],
};
}
let moveendTimeout = null;
function onMapMoveEnd() {
if (moveendTimeout) clearTimeout(moveendTimeout);
moveendTimeout = setTimeout(function() {
loadAssets();
loadInspectionsHistory();
}, 400);
}
function setupGeolocation() {
const gpsBtn = document.getElementById('btn-gps-locate');
if (gpsBtn) {
gpsBtn.addEventListener('click', locateUser);
}
locateUser();
}
function locateUser() {
if (!navigator.geolocation) return;
const statusText = document.getElementById('gps-status-text');
if (statusText) statusText.textContent = 'Géolocalisation...';
navigator.geolocation.getCurrentPosition(
function(position) {
const lng = position.coords.longitude;
const lat = position.coords.latitude;
if (typeof maplibregl !== 'undefined' && maplibregl.Marker && map) {
if (userMarker) {
userMarker.setLngLat([lng, lat]);
} else {
const el = document.createElement('div');
el.className = 'user-gps-dot';
userMarker = new maplibregl.Marker({ element: el })
.setLngLat([lng, lat])
.addTo(map);
}
map.flyTo({ center: [lng, lat], zoom: 17 });
}
if (statusText) statusText.textContent = 'Position actualisée';
},
function(err) {
console.warn('GPS error:', err.message);
if (statusText) statusText.textContent = 'GPS indisponible';
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 5000 }
);
}
function updateUIElements(thematicCode, categoryId) {
const thematicSelect = document.getElementById('filter-thematic');
if (thematicSelect) {
thematicSelect.value = thematicCode || '';
}
const thematicTiles = document.querySelectorAll('.thematic-tile');
let selectedIcon = 'bi-folder2';
let selectedThematicName = 'Thématique';
thematicTiles.forEach(tile => {
const code = tile.getAttribute('data-code');
const icon = tile.getAttribute('data-icon');
const name = tile.getAttribute('data-name');
const check = tile.querySelector('.thematic-check');
if (code === thematicCode) {
tile.classList.add('active');
if (check) check.classList.remove('d-none');
selectedIcon = icon || selectedIcon;
selectedThematicName = name || selectedThematicName;
} else {
tile.classList.remove('active');
if (check) check.classList.add('d-none');
}
});
const iconDisp = document.getElementById('thematic-icon-display');
const nameDisp = document.getElementById('thematic-name-display');
if (iconDisp) iconDisp.className = `bi ${selectedIcon} text-primary me-2`;
if (nameDisp) nameDisp.textContent = selectedThematicName;
const categorySelect = document.getElementById('filter-category');
if (categorySelect) {
categorySelect.value = categoryId || '';
}
const categoryItems = document.querySelectorAll('.category-item');
let selectedCategoryName = 'Toutes les catégories';
categoryItems.forEach(item => {
const id = item.getAttribute('data-id');
const itemThematic = item.getAttribute('data-thematic');
const name = item.getAttribute('data-name');
const check = item.querySelector('.category-check');
if (!id) {
item.classList.remove('d-none');
} else if (!thematicCode || itemThematic === thematicCode) {
item.classList.remove('d-none');
} else {
item.classList.add('d-none');
}
if ((id || '') === (categoryId || '')) {
item.classList.add('active');
if (check) check.classList.remove('d-none');
selectedCategoryName = name || selectedCategoryName;
} else {
item.classList.remove('active');
if (check) check.classList.add('d-none');
}
});
const catNameDisp = document.getElementById('category-name-display');
if (catNameDisp) catNameDisp.textContent = selectedCategoryName;
}
function initFilterStorage() {
const savedThematic = localStorage.getItem(STORAGE_THEMATIC_KEY) || 'parking';
const savedCategory = localStorage.getItem(STORAGE_CATEGORY_KEY) || '';
updateUIElements(savedThematic, savedCategory);
const thematicTiles = document.querySelectorAll('.thematic-tile');
thematicTiles.forEach(tile => {
tile.addEventListener('click', function() {
const code = this.getAttribute('data-code');
localStorage.setItem(STORAGE_THEMATIC_KEY, code);
localStorage.removeItem(STORAGE_CATEGORY_KEY);
updateUIElements(code, '');
loadAssets();
loadInspectionsHistory();
});
});
const categoryItems = document.querySelectorAll('.category-item');
categoryItems.forEach(item => {
item.addEventListener('click', function() {
const id = this.getAttribute('data-id');
const thematicSelect = document.getElementById('filter-thematic');
const currentThematic = thematicSelect ? thematicSelect.value : '';
localStorage.setItem(STORAGE_CATEGORY_KEY, id || '');
updateUIElements(currentThematic, id || '');
loadAssets();
loadInspectionsHistory();
});
});
const thematicSelect = document.getElementById('filter-thematic');
if (thematicSelect) {
thematicSelect.addEventListener('change', function() {
localStorage.setItem(STORAGE_THEMATIC_KEY, thematicSelect.value);
updateUIElements(thematicSelect.value, '');
localStorage.removeItem(STORAGE_CATEGORY_KEY);
loadAssets();
loadInspectionsHistory();
});
}
const categorySelect = document.getElementById('filter-category');
if (categorySelect) {
categorySelect.addEventListener('change', function() {
const thematic = thematicSelect ? thematicSelect.value : '';
localStorage.setItem(STORAGE_CATEGORY_KEY, categorySelect.value);
updateUIElements(thematic, categorySelect.value);
loadAssets();
loadInspectionsHistory();
});
}
// Initialisation de la période d'historique des inspections
const periodItems = document.querySelectorAll('.insp-period-item');
periodItems.forEach(item => {
item.addEventListener('click', function() {
const period = this.getAttribute('data-period') || '3m';
localStorage.setItem(STORAGE_HISTORY_PERIOD_KEY, period);
updateHistoryUIElements(period);
loadInspectionsHistory();
});
});
const savedPeriod = localStorage.getItem(STORAGE_HISTORY_PERIOD_KEY) || '3m';
updateHistoryUIElements(savedPeriod);
}
const STORAGE_HISTORY_PERIOD_KEY = 'streetup_insp_history_period';
function getHistoryPeriodLabels() {
return {
'disabled': 'Inspections (Désactivé)',
'1m': 'Inspections (1 mois)',
'3m': 'Inspections (3 mois)',
'6m': 'Inspections (6 mois)',
'1y': 'Inspections (1 an)',
'all': 'Inspections (Tout)'
};
}
function updateHistoryUIElements(period) {
const periodItems = document.querySelectorAll('.insp-period-item');
const labels = getHistoryPeriodLabels();
const iconEl = document.getElementById('inspections-history-icon');
const labelEl = document.getElementById('inspections-history-label');
periodItems.forEach(item => {
const p = item.getAttribute('data-period');
const check = item.querySelector('.period-check');
if (p === period) {
item.classList.add('active');
if (check) check.classList.remove('d-none');
} else {
item.classList.remove('active');
if (check) check.classList.add('d-none');
}
});
if (labelEl) labelEl.textContent = labels[period] || 'Inspections (3 mois)';
if (iconEl) {
if (period === 'disabled') {
iconEl.className = 'bi bi-eye-slash text-muted me-1';
} else {
iconEl.className = 'bi bi-clock-history text-primary me-1';
}
}
}
function loadInspectionsHistory() {
if (!map) return;
const period = localStorage.getItem(STORAGE_HISTORY_PERIOD_KEY) || '3m';
updateHistoryUIElements(period);
const source = map.getSource('inspections-history-source');
if (!source) return;
if (period === 'disabled') {
source.setData({ type: 'FeatureCollection', features: [] });
return;
}
const currentZoom = (typeof map.getZoom === 'function') ? map.getZoom() : 0;
if (currentZoom < MIN_ZOOM_FOR_GEOJSON) {
source.setData({ type: 'FeatureCollection', features: [] });
return;
}
const thematicSelect = document.getElementById('filter-thematic');
const thematic = thematicSelect ? thematicSelect.value : '';
const categorySelect = document.getElementById('filter-category');
const categoryId = categorySelect ? categorySelect.value : '';
const payload = {
thematic: thematic,
category: categoryId,
period: period,
filter_geom: getMapBbox()
};
fetch('/assets/inspections/geojson/', {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
})
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data && data.features) {
source.setData(data);
}
})
.catch(err => console.warn('Erreur chargement GeoJSON inspections:', err));
}
const MIN_ZOOM_FOR_GEOJSON = 16;
function loadAssets() {
if (!map) return;
const warningAlert = document.getElementById('map-warning-alert');
const currentZoom = (typeof map.getZoom === 'function') ? map.getZoom() : 0;
if (currentZoom < MIN_ZOOM_FOR_GEOJSON) {
if (warningAlert) warningAlert.classList.remove('d-none');
const source = map.getSource('assets-source');
if (source) {
source.setData({ type: 'FeatureCollection', features: [] });
}
loadedAssets = [];
return;
}
const thematicSelect = document.getElementById('filter-thematic');
const thematic = thematicSelect ? thematicSelect.value : '';
const categorySelect = document.getElementById('filter-category');
const categoryId = categorySelect ? categorySelect.value : '';
let fetchUrl = '/assets/parking/assets/geojson/';
if (thematic === 'parking') {
fetchUrl = '/assets/parking/assets/geojson/';
} else if (thematic === 'trafficlights') {
fetchUrl = '/assets/trafficlights/assets/geojson/';
} else if (thematic === 'publiclighting') {
fetchUrl = '/assets/publiclighting/assets/geojson/';
}
const payload = {};
if (categoryId) {
payload.category = [categoryId];
}
const bbox = getMapBbox();
if (bbox) {
payload.filter_geom = { type: 'Feature', geometry: bbox };
}
fetch(fetchUrl, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
})
.then(res => {
if (res.status === 413) {
if (warningAlert) warningAlert.classList.remove('d-none');
return null;
}
if (!res.ok) {
if (warningAlert) warningAlert.classList.remove('d-none');
return null;
}
return res.json();
})
.then(data => {
if (!data) return;
if (data.error) {
if (warningAlert) warningAlert.classList.remove('d-none');
return;
}
if (warningAlert) warningAlert.classList.add('d-none');
if (data.features) {
loadedAssets = data.features;
const source = map.getSource('assets-source');
if (source) {
source.setData(data);
}
}
})
.catch(err => {
console.error('Error loading asset GeoJSON:', err);
if (warningAlert) warningAlert.classList.remove('d-none');
});
}
function fitAllAssets() {
if (!loadedAssets || loadedAssets.length === 0 || !map) return;
const bounds = new maplibregl.LngLatBounds();
loadedAssets.forEach(f => {
if (f.geometry) {
if (f.geometry.type === 'Point') {
bounds.extend(f.geometry.coordinates);
} else if (f.geometry.type === 'Polygon') {
f.geometry.coordinates[0].forEach(coord => bounds.extend(coord));
} else if (f.geometry.type === 'MultiPolygon') {
f.geometry.coordinates.forEach(poly => poly[0].forEach(coord => bounds.extend(coord)));
}
}
});
if (!bounds.isEmpty()) {
map.fitBounds(bounds, { padding: 40 });
}
}
function handleAssetClick(feature) {
const props = feature.properties || {};
openInspectionModal(props);
}
function openFullscreenPhotoCarousel(images, startIndex) {
const overlay = document.getElementById('fullscreenPhotoOverlay');
const fsInner = document.getElementById('fullscreen-carousel-inner');
const fsIndicators = document.getElementById('fullscreen-carousel-indicators');
if (!overlay || !fsInner) return;
fsInner.innerHTML = '';
if (fsIndicators) fsIndicators.innerHTML = '';
images.forEach((doc, idx) => {
const activeClass = idx === startIndex ? 'active' : '';
const item = document.createElement('div');
item.className = `carousel-item ${activeClass} w-100 h-100`;
item.innerHTML = `
<div class="w-100 h-100 d-flex flex-column align-items-center justify-content-center p-3">
<img src="${doc.url}" class="img-fluid mh-100 mw-100 shadow-lg" style="object-fit: contain; max-height: 85vh;" alt="${doc.filename}">
<div class="text-white small mt-2 bg-dark bg-opacity-75 px-3 py-1 rounded-pill">${doc.filename}</div>
</div>
`;
fsInner.appendChild(item);
if (fsIndicators && images.length > 1) {
const ind = document.createElement('button');
ind.type = 'button';
ind.setAttribute('data-bs-target', '#fullscreenPhotoCarousel');
ind.setAttribute('data-bs-slide-to', idx.toString());
if (idx === startIndex) {
ind.className = 'active';
ind.setAttribute('aria-current', 'true');
}
ind.setAttribute('aria-label', `Photo ${idx + 1}`);
fsIndicators.appendChild(ind);
}
});
overlay.classList.remove('d-none');
const btnClose = document.getElementById('btn-close-photo-overlay');
if (btnClose) {
const closeHandler = function(e) {
if (e) e.preventDefault();
overlay.classList.add('d-none');
btnClose.removeEventListener('click', closeHandler);
};
btnClose.addEventListener('click', closeHandler);
}
const escHandler = function(e) {
if (e.key === 'Escape') {
overlay.classList.add('d-none');
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
function showInspectionDetailModal(insp) {
const modalEl = document.getElementById('mapInspectionDetailModal');
if (!modalEl) return;
const assetLink = document.getElementById('map-insp-asset-link');
const assetLinkContainer = document.getElementById('map-insp-asset-link-container');
if (insp.mobile_asset_url && assetLink) {
assetLink.href = insp.mobile_asset_url;
if (assetLinkContainer) assetLinkContainer.classList.remove('d-none');
} else if (assetLinkContainer) {
assetLinkContainer.classList.add('d-none');
}
document.getElementById('map-insp-target-display').textContent = insp.target_display || '-';
document.getElementById('map-insp-date').textContent = insp.date || '-';
const badgeEl = document.getElementById('map-insp-status-badge');
if (badgeEl) {
badgeEl.className = 'badge ' + (insp.status_badge_class || 'bg-primary');
badgeEl.textContent = insp.result_status_display || '-';
}
document.getElementById('map-insp-nature').textContent = insp.nature_display || '-';
const inspText = (insp.inspector_name || '-') + (insp.inspector_role_display ? ' (' + insp.inspector_role_display + ')' : '');
document.getElementById('map-insp-inspector').textContent = inspText;
document.getElementById('map-insp-summary').textContent = insp.details_summary || 'Aucun constat spécifique.';
const notesEl = document.getElementById('map-insp-notes');
if (notesEl) {
if (insp.notes) {
notesEl.textContent = insp.notes;
notesEl.classList.remove('d-none');
} else {
notesEl.classList.add('d-none');
}
}
const carouselContainer = document.getElementById('mapInspectionPhotosCarouselContainer');
const carouselInner = document.getElementById('map-insp-carousel-inner');
const carouselIndicators = document.getElementById('map-insp-carousel-indicators');
const photosCount = document.getElementById('map-insp-photos-count');
const docsSection = document.getElementById('map-insp-docs-section');
const docsContainer = document.getElementById('map-insp-docs-container');
if (insp.documents && insp.documents.length > 0) {
const images = insp.documents.filter(d => d.is_image);
const otherDocs = insp.documents.filter(d => !d.is_image);
if (images.length > 0 && carouselContainer && carouselInner) {
carouselInner.innerHTML = '';
if (carouselIndicators) carouselIndicators.innerHTML = '';
if (photosCount) photosCount.textContent = images.length;
images.forEach((doc, idx) => {
const activeClass = idx === 0 ? 'active' : '';
const item = document.createElement('div');
item.className = `carousel-item ${activeClass}`;
item.innerHTML = `
<div class="d-flex align-items-center justify-content-center bg-black position-relative" style="height: 280px;">
<img src="${doc.url}" class="d-block img-fluid mh-100 mw-100 insp-carousel-trigger" style="object-fit: contain; max-height: 280px; cursor: pointer;" alt="${doc.filename}" data-img-index="${idx}">
<button type="button" class="btn btn-sm btn-dark bg-opacity-75 text-white rounded-circle p-1.5 shadow insp-carousel-fullscreen-btn position-absolute bottom-0 end-0 m-2" style="z-index: 10; cursor: pointer;" data-img-index="${idx}" title="Plein écran">
<i class="bi bi-arrows-fullscreen"></i>
</button>
</div>
`;
carouselInner.appendChild(item);
if (carouselIndicators) {
const ind = document.createElement('button');
ind.type = 'button';
ind.setAttribute('data-bs-target', '#mapInspectionPhotosCarousel');
ind.setAttribute('data-bs-slide-to', idx.toString());
if (idx === 0) {
ind.className = 'active';
ind.setAttribute('aria-current', 'true');
}
ind.setAttribute('aria-label', `Photo ${idx + 1}`);
carouselIndicators.appendChild(ind);
}
});
setTimeout(function() {
const triggers = carouselInner.querySelectorAll('.insp-carousel-trigger, .insp-carousel-fullscreen-btn');
triggers.forEach(trig => {
trig.addEventListener('click', function(e) {
if (e) e.preventDefault();
const idx = parseInt(this.getAttribute('data-img-index'), 10) || 0;
openFullscreenPhotoCarousel(images, idx);
});
});
}, 50);
carouselContainer.classList.remove('d-none');
} else if (carouselContainer) {
carouselContainer.classList.add('d-none');
}
if (otherDocs.length > 0 && docsSection && docsContainer) {
docsContainer.innerHTML = '';
otherDocs.forEach(doc => {
const btn = document.createElement('a');
btn.href = doc.url;
btn.target = '_blank';
btn.className = 'btn btn-sm btn-outline-secondary d-inline-flex align-items-center gap-1';
btn.innerHTML = `<i class="bi bi-file-earmark-arrow-down text-primary"></i><span class="text-truncate" style="max-width: 180px;">${doc.filename}</span>`;
docsContainer.appendChild(btn);
});
docsSection.classList.remove('d-none');
} else if (docsSection) {
docsSection.classList.add('d-none');
}
} else {
if (carouselContainer) carouselContainer.classList.add('d-none');
if (docsSection) docsSection.classList.add('d-none');
}
const modal = new bootstrap.Modal(modalEl);
modal.show();
}
window.openInspectionModal = function(props) {
const modalEl = document.getElementById('quickInspectionModal');
if (!modalEl) return;
let ctId = props.content_type_id || props.ct_id;
if (!ctId && (props.model_name === 'parkingspot' || props.spot_type || props.asset_type === 'parkingspot' || props.thematic === 'parking')) {
ctId = window.PARKING_SPOT_CT_ID;
}
const objId = props.object_id || props.id || props.pk || '';
document.getElementById('insp_content_type_id').value = ctId || '';
document.getElementById('insp_object_id').value = objId;
document.getElementById('insp_target_type').textContent = props.asset_type_display || props.model_name || 'Asset';
document.getElementById('insp_target_code').textContent = props.target_display || props.code || (objId ? ('#' + objId) : '-');
document.getElementById('insp_target_name').textContent = props.name || props.name_fr || props.target_display || '-';
document.getElementById('insp_target_status').textContent = props.latest_result_status_display || props.status_display || props.status || '-';
const parkingFields = document.getElementById('parkingSpotSpecificFields');
if (parkingFields) {
if (props.model_name === 'parkingspot' || props.spot_type) {
parkingFields.classList.remove('d-none');
document.getElementById('insp_std_stands').value = '';
document.getElementById('insp_cargo_stands').value = '';
const markingSel = document.getElementById('insp_marking_quality');
if (markingSel) markingSel.value = '';
} else {
parkingFields.classList.add('d-none');
}
}
// Réinitialisation de l'accumulateur de photos/fichiers
accumulatedInspectionFiles = [];
const previewContainer = document.getElementById('insp_photos_preview_container');
if (previewContainer) previewContainer.innerHTML = '';
const modal = new bootstrap.Modal(modalEl);
modal.show();
};
let accumulatedInspectionFiles = [];
let inspCameraStream = null;
function initInspectionPhotoHandler() {
const btnCamera = document.getElementById('insp_btn_continuous_camera');
const btnGallery = document.getElementById('insp_btn_gallery_files');
const inputCamera = document.getElementById('insp_camera_input');
const inputGallery = document.getElementById('insp_gallery_input');
const cameraModalEl = document.getElementById('inspCameraModal');
const cameraPreview = document.getElementById('inspCameraPreview');
const cameraCanvas = document.getElementById('inspCameraCanvas');
const cameraFlashOverlay = document.getElementById('inspCameraFlashOverlay');
const cameraStatus = document.getElementById('inspCameraStatus');
const cameraCaptureBtn = document.getElementById('inspCameraCaptureBtn');
const cameraStopBtn = document.getElementById('inspCameraStopBtn');
let cameraModal = null;
if (cameraModalEl && typeof bootstrap !== 'undefined' && bootstrap.Modal) {
cameraModal = bootstrap.Modal.getOrCreateInstance(cameraModalEl);
}
if (!btnCamera || !btnGallery || !inputCamera || !inputGallery) return;
function updateCameraStatus(msg, isError) {
if (!cameraStatus) return;
cameraStatus.textContent = msg || '';
cameraStatus.className = isError ? 'small text-center text-danger my-2 flex-shrink-0' : 'small text-center text-info my-2 flex-shrink-0';
}
function triggerFlash() {
if (!cameraFlashOverlay) return;
cameraFlashOverlay.style.opacity = '1';
setTimeout(() => { cameraFlashOverlay.style.opacity = '0'; }, 150);
}
async function startCameraStream() {
if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
return false;
}
try {
updateCameraStatus('Initialisation de la caméra...', false);
inspCameraStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment', width: { ideal: 1920 }, height: { ideal: 1080 } },
audio: false
});
if ('srcObject' in cameraPreview) {
cameraPreview.srcObject = inspCameraStream;
} else {
cameraPreview.src = URL.createObjectURL(inspCameraStream);
}
cameraPreview.play();
updateCameraStatus('Caméra prête. Appuyez sur Capturer pour enregistrer une photo.', false);
return true;
} catch (err) {
console.warn('Erreur caméra canvas:', err);
stopCameraStream();
return false;
}
}
function stopCameraStream() {
if (inspCameraStream) {
inspCameraStream.getTracks().forEach(t => t.stop());
inspCameraStream = null;
}
if (cameraPreview) cameraPreview.srcObject = null;
updateCameraStatus('', false);
}
function captureFrame() {
if (!cameraPreview || !cameraCanvas) return;
const w = cameraPreview.videoWidth;
const h = cameraPreview.videoHeight;
if (!w || !h) {
updateCameraStatus('La caméra n\'est pas encore prête.', true);
return;
}
cameraCanvas.width = w;
cameraCanvas.height = h;
const ctx = cameraCanvas.getContext('2d');
ctx.drawImage(cameraPreview, 0, 0, w, h);
triggerFlash();
cameraCanvas.toBlob(function(blob) {
if (!blob) return;
const filename = `inspection_photo_${Date.now()}.jpg`;
const file = new File([blob], filename, { type: 'image/jpeg' });
accumulatedInspectionFiles.push(file);
renderPhotoPreviews();
updateCameraStatus('Photo capturée avec succès !', false);
}, 'image/jpeg', 0.9);
}
btnCamera.addEventListener('click', async function() {
if (cameraModal) {
const ok = await startCameraStream();
if (ok) {
cameraModal.show();
return;
}
}
// Fallback si le flux caméra (getUserMedia) n'est pas supporté
inputCamera.value = '';
inputCamera.click();
});
if (cameraCaptureBtn) {
cameraCaptureBtn.addEventListener('click', captureFrame);
}
if (cameraStopBtn) {
cameraStopBtn.addEventListener('click', function() {
if (cameraModal) cameraModal.hide();
});
}
if (cameraModalEl) {
cameraModalEl.addEventListener('hidden.bs.modal', function() {
stopCameraStream();
});
}
btnGallery.addEventListener('click', function() {
inputGallery.value = '';
inputGallery.click();
});
function handleFileSelection(files) {
Array.from(files).forEach(file => {
accumulatedInspectionFiles.push(file);
});
renderPhotoPreviews();
}
inputCamera.addEventListener('change', function(e) {
if (e.target.files && e.target.files.length > 0) {
handleFileSelection(e.target.files);
}
});
inputGallery.addEventListener('change', function(e) {
if (e.target.files && e.target.files.length > 0) {
handleFileSelection(e.target.files);
}
});
function renderPhotoPreviews() {
const previewContainer = document.getElementById('insp_photos_preview_container');
if (!previewContainer) return;
previewContainer.innerHTML = '';
accumulatedInspectionFiles.forEach((file, index) => {
const col = document.createElement('div');
col.className = 'col-4 col-sm-3 position-relative mt-2';
const card = document.createElement('div');
card.className = 'border rounded overflow-hidden shadow-sm bg-white text-center p-1 position-relative';
card.style.height = '75px';
if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'cover';
card.appendChild(img);
} else {
const icon = document.createElement('div');
icon.className = 'd-flex flex-column align-items-center justify-content-center h-100 text-muted';
icon.innerHTML = '<i class="bi bi-file-earmark fs-4 text-primary"></i><span class="extra-small text-truncate w-100 px-1">' + file.name + '</span>';
card.appendChild(icon);
}
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn-danger btn-sm rounded-circle position-absolute top-0 end-0 p-0 d-flex align-items-center justify-content-center';
removeBtn.style.width = '20px';
removeBtn.style.height = '20px';
removeBtn.style.fontSize = '12px';
removeBtn.style.transform = 'translate(30%, -30%)';
removeBtn.innerHTML = '&times;';
removeBtn.onclick = function(ev) {
ev.stopPropagation();
accumulatedInspectionFiles.splice(index, 1);
renderPhotoPreviews();
};
card.appendChild(removeBtn);
col.appendChild(card);
previewContainer.appendChild(col);
});
}
}
function initFormHandler() {
initInspectionPhotoHandler();
const form = document.getElementById('createInspectionForm') || document.getElementById('quickInspectionForm');
if (!form) return;
form.addEventListener('submit', function(e) {
e.preventDefault();
const alertDiv = document.getElementById('quickInspectionAlert');
const submitBtn = document.getElementById('submitQuickInspectionBtn');
const spinner = document.getElementById('submitQuickInspectionSpinner');
const icon = document.getElementById('submitQuickInspectionIcon');
alertDiv.classList.add('d-none');
alertDiv.textContent = '';
submitBtn.disabled = true;
if (spinner) spinner.classList.remove('d-none');
if (icon) icon.classList.add('d-none');
const formData = new FormData(form);
// Ajout des fichiers accumulés en mode continu
accumulatedInspectionFiles.forEach(file => {
formData.append('photos', file);
});
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
const modalEl = document.getElementById('quickInspectionModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
let msg = data.message || 'Inspection enregistrée !';
if (data.created_intervention_code) {
msg += ' Intervention ' + data.created_intervention_code + ' créée.';
}
alert(msg);
loadAssets();
} else {
alertDiv.textContent = data.error || 'Erreur lors de la validation.';
alertDiv.classList.remove('d-none');
}
})
.catch(err => {
alertDiv.textContent = 'Erreur réseau.';
alertDiv.classList.remove('d-none');
})
.finally(() => {
submitBtn.disabled = false;
if (spinner) spinner.classList.add('d-none');
if (icon) icon.classList.remove('d-none');
});
});
}
function getCsrfToken() {
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
return cookieValue || '';
}
})();