898 lines
30 KiB
HTML
898 lines
30 KiB
HTML
{% extends "mobile/mobile_base.html" %}
|
|
{% load static %}
|
|
{% load i18n %}
|
|
|
|
{% block extra_head %}
|
|
<!-- MapLibre GL -->
|
|
<link href="{% static 'common/maplibre-gl.css' %}" rel="stylesheet">
|
|
|
|
<style>
|
|
/* === CARTE === */
|
|
#map-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
z-index: 2000;
|
|
background: #fff;
|
|
}
|
|
#map-overlay.active {
|
|
display: block;
|
|
}
|
|
#observation-map {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
#map-close-btn {
|
|
position: absolute;
|
|
top: 0.6rem; right: 0.6rem;
|
|
z-index: 2100;
|
|
}
|
|
/* FAB bouton carte : au-dessus du footer */
|
|
#map-fab-btn {
|
|
position: fixed;
|
|
bottom: 4.5rem;
|
|
right: 1rem;
|
|
z-index: 1050;
|
|
border-radius: 50%;
|
|
width: 3.25rem;
|
|
height: 3.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
|
padding: 0;
|
|
}
|
|
.maplibregl-popup-content a {
|
|
color: #0d6efd;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.observation-card {
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.observation-card:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.form-select, .form-control {
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Boutons de filtre : même largeur et centrage du texte */
|
|
#typeFilterPanel .btn-outline-type,
|
|
#statusFilterPanel .btn-outline-status {
|
|
width: 110px;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Type button colors and styles */
|
|
.btn-outline-type-incident {
|
|
--bs-btn-color: #dc3545;
|
|
--bs-btn-border-color: #dc3545;
|
|
--bs-btn-hover-color: #fff;
|
|
--bs-btn-hover-bg: #dc3545;
|
|
--bs-btn-hover-border-color: #dc3545;
|
|
--bs-btn-active-color: #fff;
|
|
--bs-btn-active-bg: #dc3545;
|
|
--bs-btn-active-border-color: #dc3545;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-type-incident {
|
|
background-color: #dc3545;
|
|
border-color: #dc3545;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-outline-type-inventory {
|
|
--bs-btn-color: #6c757d;
|
|
--bs-btn-border-color: #6c757d;
|
|
--bs-btn-hover-color: #fff;
|
|
--bs-btn-hover-bg: #6c757d;
|
|
--bs-btn-hover-border-color: #6c757d;
|
|
--bs-btn-active-color: #fff;
|
|
--bs-btn-active-bg: #6c757d;
|
|
--bs-btn-active-border-color: #6c757d;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-type-inventory {
|
|
background-color: #6c757d;
|
|
border-color: #6c757d;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-outline-type-remark {
|
|
--bs-btn-color: #0dcaf0;
|
|
--bs-btn-border-color: #0dcaf0;
|
|
--bs-btn-hover-color: #000;
|
|
--bs-btn-hover-bg: #0dcaf0;
|
|
--bs-btn-hover-border-color: #0dcaf0;
|
|
--bs-btn-active-color: #000;
|
|
--bs-btn-active-bg: #0dcaf0;
|
|
--bs-btn-active-border-color: #0dcaf0;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-type-remark {
|
|
background-color: #0dcaf0;
|
|
border-color: #0dcaf0;
|
|
color: #000;
|
|
}
|
|
|
|
/* Status button colors */
|
|
.btn-outline-status-in_preparation {
|
|
--bs-btn-color: #6c757d;
|
|
--bs-btn-border-color: #6c757d;
|
|
--bs-btn-hover-color: #fff;
|
|
--bs-btn-hover-bg: #6c757d;
|
|
--bs-btn-hover-border-color: #6c757d;
|
|
--bs-btn-active-color: #fff;
|
|
--bs-btn-active-bg: #6c757d;
|
|
--bs-btn-active-border-color: #6c757d;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-in_preparation {
|
|
background-color: #6c757d;
|
|
border-color: #6c757d;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-outline-status-to_process {
|
|
--bs-btn-color: #dc3545;
|
|
--bs-btn-border-color: #dc3545;
|
|
--bs-btn-hover-color: #fff;
|
|
--bs-btn-hover-bg: #dc3545;
|
|
--bs-btn-hover-border-color: #dc3545;
|
|
--bs-btn-active-color: #fff;
|
|
--bs-btn-active-bg: #dc3545;
|
|
--bs-btn-active-border-color: #dc3545;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-to_process {
|
|
background-color: #dc3545;
|
|
border-color: #dc3545;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-outline-status-in_progress {
|
|
--bs-btn-color: #ffc107;
|
|
--bs-btn-border-color: #ffc107;
|
|
--bs-btn-hover-color: #000;
|
|
--bs-btn-hover-bg: #ffc107;
|
|
--bs-btn-hover-border-color: #ffc107;
|
|
--bs-btn-active-color: #000;
|
|
--bs-btn-active-bg: #ffc107;
|
|
--bs-btn-active-border-color: #ffc107;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-in_progress {
|
|
background-color: #ffc107;
|
|
border-color: #ffc107;
|
|
color: #000;
|
|
}
|
|
|
|
.btn-outline-status-processed {
|
|
--bs-btn-color: #28a745;
|
|
--bs-btn-border-color: #28a745;
|
|
--bs-btn-hover-color: #fff;
|
|
--bs-btn-hover-bg: #28a745;
|
|
--bs-btn-hover-border-color: #28a745;
|
|
--bs-btn-active-color: #fff;
|
|
--bs-btn-active-bg: #28a745;
|
|
--bs-btn-active-border-color: #28a745;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-processed {
|
|
background-color: #28a745;
|
|
border-color: #28a745;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-outline-status-archived {
|
|
--bs-btn-color: #000000;
|
|
--bs-btn-border-color: #000000;
|
|
--bs-btn-hover-color: #fff;
|
|
--bs-btn-hover-bg: #000000;
|
|
--bs-btn-hover-border-color: #000000;
|
|
--bs-btn-active-color: #fff;
|
|
--bs-btn-active-bg: #000000;
|
|
--bs-btn-active-border-color: #000000;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-archived {
|
|
background-color: #000000;
|
|
border-color: #000000;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-outline-status-closed {
|
|
--bs-btn-color: #343a40;
|
|
--bs-btn-border-color: #343a40;
|
|
--bs-btn-hover-color: #fff;
|
|
--bs-btn-hover-bg: #343a40;
|
|
--bs-btn-hover-border-color: #343a40;
|
|
--bs-btn-active-color: #fff;
|
|
--bs-btn-active-bg: #343a40;
|
|
--bs-btn-active-border-color: #343a40;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-closed {
|
|
background-color: #343a40;
|
|
border-color: #343a40;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-outline-status-not_relevant {
|
|
--bs-btn-color: #6c757d;
|
|
--bs-btn-border-color: #6c757d;
|
|
--bs-btn-hover-color: #fff;
|
|
--bs-btn-hover-bg: #6c757d;
|
|
--bs-btn-hover-border-color: #6c757d;
|
|
--bs-btn-active-color: #fff;
|
|
--bs-btn-active-bg: #6c757d;
|
|
--bs-btn-active-border-color: #6c757d;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-not_relevant {
|
|
background-color: #6c757d;
|
|
border-color: #6c757d;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-outline-status-to_redirect {
|
|
--bs-btn-color: #ffc107;
|
|
--bs-btn-border-color: #ffc107;
|
|
--bs-btn-hover-color: #000;
|
|
--bs-btn-hover-bg: #ffc107;
|
|
--bs-btn-hover-border-color: #ffc107;
|
|
--bs-btn-active-color: #000;
|
|
--bs-btn-active-bg: #ffc107;
|
|
--bs-btn-active-border-color: #ffc107;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-to_redirect {
|
|
background-color: #ffc107;
|
|
border-color: #ffc107;
|
|
color: #000;
|
|
}
|
|
|
|
.btn-outline-status-duplicate {
|
|
--bs-btn-color: #0dcaf0;
|
|
--bs-btn-border-color: #0dcaf0;
|
|
--bs-btn-hover-color: #000;
|
|
--bs-btn-hover-bg: #0dcaf0;
|
|
--bs-btn-hover-border-color: #0dcaf0;
|
|
--bs-btn-active-color: #000;
|
|
--bs-btn-active-bg: #0dcaf0;
|
|
--bs-btn-active-border-color: #0dcaf0;
|
|
}
|
|
|
|
.btn-check:checked + .btn-outline-status-duplicate {
|
|
background-color: #0dcaf0;
|
|
border-color: #0dcaf0;
|
|
color: #000;
|
|
}
|
|
|
|
/* Spinner de chargement */
|
|
#loading-spinner {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 10;
|
|
}
|
|
|
|
#observations-list {
|
|
position: relative;
|
|
min-height: 100px;
|
|
}
|
|
|
|
#observations-list.loading {
|
|
opacity: 0.3;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
{% endblock extra_head %}
|
|
|
|
{% block title %}
|
|
{% translate "Observations" %}
|
|
{% endblock title %}
|
|
|
|
{% block content %}
|
|
|
|
<!-- Filters -->
|
|
<form method="get" class="d-flex flex-column gap-2 mb-3">
|
|
<!-- Add observation button -->
|
|
<div class="d-grid mb-3">
|
|
<a href="{% url 'mobile:observation_add_mobile' %}" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle"></i> {% translate "Ajouter une observation" %}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Filter buttons -->
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<div class="form-check m-0">
|
|
<input
|
|
type="checkbox"
|
|
class="form-check-input"
|
|
name="mine"
|
|
id="filter-mine"
|
|
value="1"
|
|
{% if show_mine_only %}checked{% endif %}
|
|
data-autosubmit
|
|
>
|
|
<label class="form-check-label" for="filter-mine">
|
|
<i class="bi bi-person"></i> {% translate "Mes observations" %}
|
|
</label>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
{% if type_filters %}
|
|
<button id="toggle-type-filter"
|
|
class="btn btn-outline-secondary btn-sm"
|
|
type="button"
|
|
aria-expanded="false"
|
|
aria-controls="typeFilterPanel"
|
|
title="{% translate 'Filtrer par type' %}">
|
|
<i class="bi bi-tag"></i>
|
|
<span id="typeCountBadge" class="badge text-bg-primary ms-1 d-none">0</span>
|
|
</button>
|
|
{% endif %}
|
|
{% if status_filters %}
|
|
<button id="toggle-status-filter"
|
|
class="btn btn-outline-secondary btn-sm"
|
|
type="button"
|
|
aria-expanded="false"
|
|
aria-controls="statusFilterPanel"
|
|
title="{% translate 'Filtrer par statut' %}">
|
|
<i class="bi bi-funnel"></i>
|
|
<span id="statusCountBadge" class="badge text-bg-primary ms-1 d-none">0</span>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Type filter panel -->
|
|
{% if type_filters %}
|
|
<div id="typeFilterPanel" class="collapse mb-2">
|
|
<p class="mb-1 fw-semibold small text-uppercase text-muted">{% translate "Types" %}</p>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
{% for type in type_filters %}
|
|
{% with field_id="type-filter-"|add:type.key %}
|
|
<input
|
|
type="checkbox"
|
|
class="btn-check"
|
|
name="type"
|
|
id="{{ field_id }}"
|
|
value="{{ type.key }}"
|
|
{% if type.checked %}checked{% endif %}
|
|
data-type-filter
|
|
autocomplete="off"
|
|
>
|
|
<label class="btn btn-outline-type btn-outline-type-{{ type.key }} btn-sm" for="{{ field_id }}">
|
|
{% if type.key == 'incident' %}
|
|
<i class="bi bi-exclamation-triangle-fill"></i>
|
|
{% elif type.key == 'inventory' %}
|
|
<i class="bi bi-clipboard-check"></i>
|
|
{% elif type.key == 'remark' %}
|
|
<i class="bi bi-chat-left-text"></i>
|
|
{% endif %}
|
|
{{ type.label }}
|
|
</label>
|
|
{% endwith %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Status filter panel -->
|
|
{% if status_filters %}
|
|
<div id="statusFilterPanel" class="collapse mb-2">
|
|
<p class="mb-1 fw-semibold small text-uppercase text-muted">{% translate "Statuts" %}</p>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
{% for status in status_filters %}
|
|
{% with field_id="status-filter-"|add:status.key %}
|
|
<input
|
|
type="checkbox"
|
|
class="btn-check"
|
|
name="status"
|
|
id="{{ field_id }}"
|
|
value="{{ status.key }}"
|
|
{% if status.checked %}checked{% endif %}
|
|
data-status-filter
|
|
autocomplete="off"
|
|
>
|
|
<label class="btn btn-outline-status btn-outline-status-{{ status.key }} btn-sm" for="{{ field_id }}">{{ status.label }}</label>
|
|
{% endwith %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Search field -->
|
|
<div class="input-group">
|
|
<button type="button" class="btn btn-outline-secondary" id="clearSearch" title="Effacer" aria-label="Effacer la recherche">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
|
|
<input
|
|
type="text"
|
|
name="q"
|
|
id="searchInput"
|
|
class="form-control"
|
|
placeholder="{% translate 'Rechercher...' %}"
|
|
value="{{ search_query }}"
|
|
>
|
|
|
|
<button type="submit" class="btn btn-primary" title="Rechercher">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
<!-- Bouton flottant carte -->
|
|
<button id="map-fab-btn" class="btn btn-primary" title="{% translate 'Voir la carte' %}" aria-label="{% translate 'Voir la carte' %}">
|
|
<i class="bi bi-map fs-5"></i>
|
|
</button>
|
|
|
|
<!-- Overlay pleine page : carte -->
|
|
<div id="map-overlay" role="dialog" aria-modal="true" aria-label="{% translate 'Carte des observations' %}">
|
|
<button id="map-close-btn" class="btn btn-primary shadow-sm" aria-label="{% translate 'Fermer la carte' %}">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
<div id="observation-map"></div>
|
|
</div>
|
|
|
|
<!-- Observations list -->
|
|
<div id="observations-list">
|
|
<!-- Loading spinner -->
|
|
<div id="loading-spinner" class="d-none">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">{% translate "Chargement..." %}</span>
|
|
</div>
|
|
</div>
|
|
<div id="observations-list-content">
|
|
{% include 'observations/_observations_list_mobile.html' %}
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock content %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const form = document.querySelector('form[method="get"]');
|
|
if (!form) return;
|
|
|
|
const input = document.getElementById('searchInput');
|
|
const clearBtn = document.getElementById('clearSearch');
|
|
const listContainer = document.getElementById('observations-list');
|
|
const listContent = document.getElementById('observations-list-content');
|
|
const spinner = document.getElementById('loading-spinner');
|
|
|
|
// Latence pour éviter les soumissions multiples rapides
|
|
let submitTimer = null;
|
|
const SUBMIT_DELAY = 250; // 250ms de latence
|
|
let requestCounter = 0;
|
|
|
|
function showSpinner() {
|
|
if (spinner) spinner.classList.remove('d-none');
|
|
if (listContainer) listContainer.classList.add('loading');
|
|
}
|
|
|
|
function hideSpinner() {
|
|
if (spinner) spinner.classList.add('d-none');
|
|
if (listContainer) listContainer.classList.remove('loading');
|
|
}
|
|
|
|
async function submitForm(preserveFocus) {
|
|
const requestId = ++requestCounter;
|
|
|
|
clearTimeout(submitTimer);
|
|
submitTimer = setTimeout(async () => {
|
|
showSpinner();
|
|
|
|
const formData = new FormData(form);
|
|
const params = new URLSearchParams();
|
|
for (const [key, value] of formData.entries()) {
|
|
if (value !== '') params.append(key, value);
|
|
}
|
|
|
|
const queryString = params.toString();
|
|
const action = form.getAttribute('action') || window.location.pathname;
|
|
const targetUrl = queryString ? `${action}?${queryString}` : action;
|
|
|
|
try {
|
|
const response = await fetch(targetUrl, {
|
|
method: 'GET',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const payload = await response.json();
|
|
if (requestId !== requestCounter) return;
|
|
|
|
if (payload.html !== undefined && (listContent || listContainer)) {
|
|
if (listContent) {
|
|
listContent.innerHTML = payload.html;
|
|
} else {
|
|
listContainer.innerHTML = payload.html; // fallback
|
|
}
|
|
initCardsIfNeeded();
|
|
}
|
|
|
|
// Mettre à jour les marqueurs de la carte
|
|
if (payload.markers !== undefined) {
|
|
document.dispatchEvent(new CustomEvent('observations-markers-updated', {
|
|
detail: { markers: payload.markers }
|
|
}));
|
|
}
|
|
|
|
const historyUrl = queryString ? `${window.location.pathname}?${queryString}` : window.location.pathname;
|
|
window.history.replaceState(null, '', historyUrl);
|
|
|
|
if (preserveFocus && input) {
|
|
input.focus({ preventScroll: true });
|
|
const len = input.value.length;
|
|
if (typeof input.setSelectionRange === 'function') input.setSelectionRange(len, len);
|
|
}
|
|
} catch (error) {
|
|
console.error('AJAX error:', error);
|
|
form.removeEventListener('submit', handleSubmit);
|
|
form.submit(); // fallback full reload
|
|
} finally {
|
|
hideSpinner();
|
|
}
|
|
}, SUBMIT_DELAY);
|
|
}
|
|
|
|
async function handleSubmit(event) {
|
|
event.preventDefault();
|
|
const preserveFocus = !!input && (document.activeElement === input);
|
|
await submitForm(preserveFocus);
|
|
}
|
|
|
|
form.addEventListener('submit', handleSubmit);
|
|
|
|
// Clear search button
|
|
if (clearBtn && input) {
|
|
clearBtn.addEventListener('click', function() {
|
|
input.value = '';
|
|
submitForm(true);
|
|
});
|
|
}
|
|
|
|
// Autosubmit on generic data-autosubmit elements (e.g., mine checkbox)
|
|
const autoSubmitTargets = form.querySelectorAll('[data-autosubmit]');
|
|
autoSubmitTargets.forEach((el) => {
|
|
el.addEventListener('change', () => submitForm(false));
|
|
});
|
|
|
|
// Search input with debounce
|
|
if (input) {
|
|
input.addEventListener('input', () => {
|
|
const value = (input.value || '').trim();
|
|
if (value.length === 0 || value.length >= 2) {
|
|
submitForm(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Gestion de la persistence des panneaux de filtres
|
|
const STORAGE_KEY_TYPE = 'obs_typeFilterOpen';
|
|
const STORAGE_KEY_STATUS = 'obs_statusFilterOpen';
|
|
|
|
// Type filter collapse
|
|
const typePanel = document.getElementById('typeFilterPanel');
|
|
const typeBtn = document.getElementById('toggle-type-filter');
|
|
const typeBadge = document.getElementById('typeCountBadge');
|
|
const typeChecks = Array.from(form.querySelectorAll('input[data-type-filter]'));
|
|
|
|
if (typePanel && typeBtn) {
|
|
const typeCollapse = bootstrap.Collapse.getOrCreateInstance(typePanel, { toggle: false });
|
|
|
|
// Restaurer l'état depuis sessionStorage
|
|
const typeWasOpen = sessionStorage.getItem(STORAGE_KEY_TYPE) === 'true';
|
|
if (typeWasOpen) {
|
|
typeCollapse.show();
|
|
}
|
|
|
|
// Sauvegarder l'état lors des changements
|
|
typePanel.addEventListener('shown.bs.collapse', () => {
|
|
typeBtn.setAttribute('aria-expanded', 'true');
|
|
sessionStorage.setItem(STORAGE_KEY_TYPE, 'true');
|
|
});
|
|
typePanel.addEventListener('hidden.bs.collapse', () => {
|
|
typeBtn.setAttribute('aria-expanded', 'false');
|
|
sessionStorage.setItem(STORAGE_KEY_TYPE, 'false');
|
|
});
|
|
|
|
typeBtn.addEventListener('click', () => typeCollapse.toggle());
|
|
}
|
|
|
|
function updateTypeCountBadge() {
|
|
const count = typeChecks.filter(c => c.checked).length;
|
|
if (!typeBadge) return;
|
|
if (count > 0) {
|
|
typeBadge.textContent = count;
|
|
typeBadge.classList.remove('d-none');
|
|
} else {
|
|
typeBadge.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
// Type checkboxes: at least one must be checked + autosubmit avec latence
|
|
typeChecks.forEach((checkbox) => {
|
|
checkbox.addEventListener('change', (event) => {
|
|
if (!event.target.checked) {
|
|
const otherChecked = typeChecks.some((cb) => cb !== event.target && cb.checked);
|
|
if (!otherChecked) {
|
|
event.target.checked = true;
|
|
return;
|
|
}
|
|
}
|
|
updateTypeCountBadge();
|
|
submitForm(false);
|
|
});
|
|
});
|
|
updateTypeCountBadge();
|
|
|
|
// Status filter collapse
|
|
const statusPanel = document.getElementById('statusFilterPanel');
|
|
const statusBtn = document.getElementById('toggle-status-filter');
|
|
const statusBadge = document.getElementById('statusCountBadge');
|
|
const statusChecks = Array.from(form.querySelectorAll('input[data-status-filter]'));
|
|
|
|
if (statusPanel && statusBtn) {
|
|
const statusCollapse = bootstrap.Collapse.getOrCreateInstance(statusPanel, { toggle: false });
|
|
|
|
// Restaurer l'état depuis sessionStorage
|
|
const statusWasOpen = sessionStorage.getItem(STORAGE_KEY_STATUS) === 'true';
|
|
if (statusWasOpen) {
|
|
statusCollapse.show();
|
|
}
|
|
|
|
// Sauvegarder l'état lors des changements
|
|
statusPanel.addEventListener('shown.bs.collapse', () => {
|
|
statusBtn.setAttribute('aria-expanded', 'true');
|
|
sessionStorage.setItem(STORAGE_KEY_STATUS, 'true');
|
|
});
|
|
statusPanel.addEventListener('hidden.bs.collapse', () => {
|
|
statusBtn.setAttribute('aria-expanded', 'false');
|
|
sessionStorage.setItem(STORAGE_KEY_STATUS, 'false');
|
|
});
|
|
|
|
statusBtn.addEventListener('click', () => statusCollapse.toggle());
|
|
}
|
|
|
|
function updateStatusCountBadge() {
|
|
const count = statusChecks.filter(c => c.checked).length;
|
|
if (!statusBadge) return;
|
|
if (count > 0) {
|
|
statusBadge.textContent = count;
|
|
statusBadge.classList.remove('d-none');
|
|
} else {
|
|
statusBadge.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
// Status checkboxes: at least one must be checked + autosubmit avec latence
|
|
statusChecks.forEach((checkbox) => {
|
|
checkbox.addEventListener('change', (event) => {
|
|
if (!event.target.checked) {
|
|
const otherChecked = statusChecks.some((cb) => cb !== event.target && cb.checked);
|
|
if (!otherChecked) {
|
|
event.target.checked = true;
|
|
return;
|
|
}
|
|
}
|
|
updateStatusCountBadge();
|
|
submitForm(false);
|
|
});
|
|
});
|
|
updateStatusCountBadge();
|
|
|
|
// Clickable cards - initial setup
|
|
document.querySelectorAll('.card[data-href]').forEach(card => {
|
|
card.style.cursor = 'pointer';
|
|
card.addEventListener('click', e => {
|
|
if (e.target.closest('a, button, input, label, .form-check')) return;
|
|
window.location.href = card.dataset.href;
|
|
});
|
|
});
|
|
|
|
// Function to reinitialize cards after AJAX load
|
|
function initCardsIfNeeded() {
|
|
document.querySelectorAll('.card[data-href]').forEach(card => {
|
|
if (card._clickInitDone) return;
|
|
card._clickInitDone = true;
|
|
card.style.cursor = 'pointer';
|
|
card.addEventListener('click', e => {
|
|
if (e.target.closest('a, button, input, label, .form-check')) return;
|
|
window.location.href = card.dataset.href;
|
|
});
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<!-- MapLibre GL JS -->
|
|
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
|
<script>
|
|
(function () {
|
|
const STATUS_COLORS = {
|
|
'secondary': '#6c757d',
|
|
'danger': '#dc3545',
|
|
'warning': '#ffc107',
|
|
'success': '#198754',
|
|
'info': '#0dcaf0',
|
|
'dark': '#212529',
|
|
};
|
|
const STATUS_TEXT_DARK = new Set(['warning', 'info']);
|
|
|
|
let currentMarkers = {{ markers_json|safe }};
|
|
let map = null;
|
|
let fitOnOpen = true;
|
|
const SOURCE_ID = 'observations-points';
|
|
const LAYER_ID = 'observations-circles';
|
|
|
|
function hexColor(bsColor) {
|
|
return STATUS_COLORS[bsColor] || '#6c757d';
|
|
}
|
|
|
|
function toGeoJSON(markersData) {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: markersData.map(m => ({
|
|
type: 'Feature',
|
|
geometry: { type: 'Point', coordinates: [m.lon, m.lat] },
|
|
properties: {
|
|
id: m.id,
|
|
code: m.code,
|
|
description: m.description,
|
|
status_label: m.status_label,
|
|
status_color: m.status_color,
|
|
observation_type: m.observation_type,
|
|
url: m.url,
|
|
fill: hexColor(m.status_color),
|
|
},
|
|
})),
|
|
};
|
|
}
|
|
|
|
function buildPopupHtml(features) {
|
|
return features.map((f, idx) => {
|
|
const p = f.properties;
|
|
const fill = hexColor(p.status_color);
|
|
const txtCol = STATUS_TEXT_DARK.has(p.status_color) ? '#000' : '#fff';
|
|
const sep = idx > 0 ? '<hr style="margin:5px 0">' : '';
|
|
return `${sep}
|
|
<div style="margin:2px 0">
|
|
<a href="${p.url}">${p.code}</a>
|
|
<span style="display:inline-block;background:${fill};color:${txtCol};
|
|
border-radius:4px;padding:1px 6px;font-size:0.7rem;margin-left:4px">
|
|
${p.status_label}
|
|
</span>
|
|
<div style="font-size:0.82rem;color:#555;margin-top:2px">${p.description}</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function updateSource(markersData) {
|
|
if (!map) return;
|
|
const src = map.getSource(SOURCE_ID);
|
|
if (src) src.setData(toGeoJSON(markersData));
|
|
}
|
|
|
|
function initMap() {
|
|
if (map) return;
|
|
map = new maplibregl.Map({
|
|
container: 'observation-map',
|
|
style: window.MAP_STYLE_LIGHT,
|
|
center: [4.35, 50.85],
|
|
zoom: 11,
|
|
touchPitch: false,
|
|
});
|
|
|
|
map.addControl(new maplibregl.NavigationControl(), 'bottom-right');
|
|
map.addControl(new maplibregl.GeolocateControl({
|
|
positionOptions: { enableHighAccuracy: true },
|
|
trackUserLocation: false,
|
|
showUserHeading: true,
|
|
fitBoundsOptions: { maxZoom: 17 },
|
|
}), 'bottom-right');
|
|
|
|
map.on('load', () => {
|
|
map.addSource(SOURCE_ID, {
|
|
type: 'geojson',
|
|
data: toGeoJSON(currentMarkers),
|
|
});
|
|
map.addLayer({
|
|
id: LAYER_ID,
|
|
type: 'circle',
|
|
source: SOURCE_ID,
|
|
paint: {
|
|
'circle-radius': 9,
|
|
'circle-color': ['get', 'fill'],
|
|
'circle-opacity': 0.92,
|
|
'circle-stroke-width': 2,
|
|
'circle-stroke-color': '#fff',
|
|
},
|
|
});
|
|
|
|
map.on('mouseenter', LAYER_ID, () => { map.getCanvas().style.cursor = 'pointer'; });
|
|
map.on('mouseleave', LAYER_ID, () => { map.getCanvas().style.cursor = ''; });
|
|
|
|
map.on('click', LAYER_ID, (e) => {
|
|
const features = map.queryRenderedFeatures(e.point, { layers: [LAYER_ID] });
|
|
if (!features.length) return;
|
|
const seen = new Set();
|
|
const unique = features.filter(f => {
|
|
if (seen.has(f.properties.id)) return false;
|
|
seen.add(f.properties.id); return true;
|
|
});
|
|
new maplibregl.Popup({ closeButton: true, maxWidth: '320px' })
|
|
.setLngLat(e.lngLat)
|
|
.setHTML(buildPopupHtml(unique))
|
|
.addTo(map);
|
|
});
|
|
|
|
if (fitOnOpen && currentMarkers.length > 0) {
|
|
fitOnOpen = false;
|
|
fitBounds(currentMarkers);
|
|
}
|
|
});
|
|
}
|
|
|
|
function fitBounds(markersData) {
|
|
if (!markersData.length) return;
|
|
if (markersData.length === 1) {
|
|
map.flyTo({ center: [markersData[0].lon, markersData[0].lat], zoom: 16 });
|
|
return;
|
|
}
|
|
const lons = markersData.map(m => m.lon);
|
|
const lats = markersData.map(m => m.lat);
|
|
map.fitBounds(
|
|
[[Math.min(...lons), Math.min(...lats)], [Math.max(...lons), Math.max(...lats)]],
|
|
{ padding: 50, maxZoom: 16 }
|
|
);
|
|
}
|
|
|
|
function openMap() {
|
|
const overlay = document.getElementById('map-overlay');
|
|
overlay.style.display = 'block';
|
|
if (!map) {
|
|
initMap();
|
|
} else {
|
|
updateSource(currentMarkers);
|
|
setTimeout(() => map.resize(), 80);
|
|
}
|
|
}
|
|
|
|
function closeMap() {
|
|
document.getElementById('map-overlay').style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('map-fab-btn').addEventListener('click', openMap);
|
|
document.getElementById('map-close-btn').addEventListener('click', closeMap);
|
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeMap(); });
|
|
|
|
document.addEventListener('observations-markers-updated', e => {
|
|
currentMarkers = e.detail.markers || [];
|
|
fitOnOpen = false;
|
|
const overlay = document.getElementById('map-overlay');
|
|
if (map && overlay && overlay.style.display !== 'none') {
|
|
updateSource(currentMarkers);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock extra_scripts %}
|