963 lines
No EOL
37 KiB
HTML
963 lines
No EOL
37 KiB
HTML
{% load static %}
|
|
{% load i18n %}
|
|
|
|
<style>
|
|
:root {
|
|
--brand-color: rgba(47, 62, 139, 1);
|
|
--labelblue: rgba(0, 169, 252, 0.75);
|
|
--bg-light-grey: #f8f8f8;
|
|
}
|
|
|
|
label, .label, .form-label {
|
|
color: var(--labelblue)
|
|
}
|
|
|
|
/* Styles pour les thématiques en badges */
|
|
.thematics-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.thematic-badge {
|
|
width: 190px;
|
|
padding: 0.5rem 1rem;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 2rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
background: white;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
user-select: none;
|
|
}
|
|
|
|
.thematic-badge:hover {
|
|
border-color: #0d6efd;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.thematic-badge.selected {
|
|
background: #0d6efd;
|
|
color: white;
|
|
border-color: #0d6efd;
|
|
}
|
|
|
|
.thematic-badge input[type="checkbox"] {
|
|
display: none;
|
|
}
|
|
|
|
.thematic-badge span {
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Styles pour l'autocomplétion des interventions */
|
|
.interventions-autocomplete-container {
|
|
position: relative;
|
|
}
|
|
|
|
.interventions-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.intervention-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 0.75rem;
|
|
background: white;
|
|
color: #0d6efd;
|
|
border-radius: 1.5rem;
|
|
border: 1px solid #0d6efd;
|
|
font-size: 0.875rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.intervention-badge span,
|
|
.intervention-badge .badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
line-height: 1;
|
|
}
|
|
|
|
.intervention-badge .remove-btn {
|
|
background: none;
|
|
border: none;
|
|
color: #0d6efd;
|
|
font-size: 1.2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
margin: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.autocomplete-results {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 1000;
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
display: none;
|
|
}
|
|
|
|
.autocomplete-results.show {
|
|
display: block;
|
|
}
|
|
|
|
.autocomplete-item {
|
|
padding: 0.75rem 1rem;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.autocomplete-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.autocomplete-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.autocomplete-item-code {
|
|
font-weight: 600;
|
|
color: #0d6efd;
|
|
}
|
|
|
|
.autocomplete-item-title {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* Styles pour les bordures des sections avec erreurs */
|
|
.has-error {
|
|
border-left: 3px solid #dc3545;
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
/* Styles pour la carte */
|
|
#map-control {
|
|
width: 100%;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
#address-search {
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
#geolocation-btn,
|
|
#clear-map-btn {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.coordinates-info {
|
|
font-size: 0.875rem;
|
|
font-family: monospace;
|
|
background: #f8f9fa;
|
|
padding: 0.5rem;
|
|
border-radius: 0.25rem;
|
|
}
|
|
</style>
|
|
|
|
<div class="container mt-4">
|
|
<h2>
|
|
{% if control %}
|
|
{% translate "Modifier le contrôle" %} {{ control.control_code }}
|
|
{% else %}
|
|
{% translate "Ajouter un nouveau contrôle" %}
|
|
{% endif %}
|
|
</h2>
|
|
<form method="post" class="mt-3" id="control-form" autocomplete="off">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="v" value="{{ request.GET.v }}">
|
|
|
|
{# Code du contrôle - affiché en lecture seule, généré automatiquement #}
|
|
<div class="col-12 mb-3">
|
|
<label class="form-label">{% translate "Code du contrôle" %}</label>
|
|
{% if control %}
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light"><i class="bi bi-hash"></i></span>
|
|
<input type="text" class="form-control bg-light fw-semibold" value="{{ control.control_code }}" readonly disabled>
|
|
<span class="input-group-text bg-light text-muted" title="{% translate 'Non modifiable' %}">
|
|
<i class="bi bi-lock-fill"></i>
|
|
</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light"><i class="bi bi-hash"></i></span>
|
|
<input type="text" class="form-control bg-light text-muted fst-italic"
|
|
value="{% translate 'Généré automatiquement (CTRL-XXXX)' %}" readonly disabled>
|
|
<span class="input-group-text bg-light text-muted" title="{% translate 'Non modifiable' %}">
|
|
<i class="bi bi-lock-fill"></i>
|
|
</span>
|
|
</div>
|
|
<small class="form-text text-muted">
|
|
<i class="bi bi-info-circle"></i>
|
|
{% translate "Le code sera généré automatiquement lors de la création." %}
|
|
</small>
|
|
{% endif %}
|
|
</div>
|
|
<hr>
|
|
|
|
{# Affichage des erreurs globales du formulaire #}
|
|
{% if form.non_field_errors %}
|
|
<div class="alert alert-danger" role="alert">
|
|
<strong><i class="bi bi-exclamation-triangle"></i> {% translate "Erreurs :" %}</strong>
|
|
<ul class="mb-0">
|
|
{% for error in form.non_field_errors %}
|
|
<li>{{ error }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Affichage d'un message général si le formulaire a des erreurs #}
|
|
{% if form.errors and not form.non_field_errors %}
|
|
<div class="alert alert-warning" role="alert">
|
|
<i class="bi bi-exclamation-circle"></i>
|
|
<strong>{% translate "Le formulaire contient des erreurs." %}</strong>
|
|
{% translate "Veuillez corriger les champs marqués en rouge ci-dessous." %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="row">
|
|
{% for field in form %}
|
|
{% if field.name == 'thematics' %}
|
|
<div class="col-12 mb-3 {% if field.errors %}has-error{% endif %}">
|
|
<hr>
|
|
<label class="form-label">{{ field.label }}</label>
|
|
<div class="thematics-container" id="thematics-container">
|
|
{% for choice_value, choice_label in field.field.choices %}
|
|
<label class="thematic-badge {% if choice_value in field.value %}selected{% endif %}">
|
|
<input
|
|
type="checkbox"
|
|
name="thematics"
|
|
value="{{ choice_value }}"
|
|
{% if choice_value in field.value %}checked{% endif %}
|
|
>
|
|
<span>{{ choice_label }}</span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
{% if field.help_text %}
|
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
|
{% endif %}
|
|
{% if field.errors %}
|
|
<div class="alert alert-danger mt-2 mb-0 py-2" role="alert">
|
|
{% for error in field.errors %}
|
|
<div><i class="bi bi-exclamation-circle"></i> {{ error }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% elif field.name == 'interventions' %}
|
|
<div class="col-12 mb-3 {% if field.errors %}has-error{% endif %}">
|
|
<hr>
|
|
<label class="form-label">{% translate "Interventions liées" %} ({% translate "Facultatif" %})</label>
|
|
<div class="interventions-autocomplete-container">
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id="interventions-search"
|
|
placeholder="{% translate 'Rechercher une intervention...' %}"
|
|
autocomplete="off"
|
|
>
|
|
<div class="autocomplete-results" id="autocomplete-results"></div>
|
|
</div>
|
|
<div class="interventions-list" id="interventions-list"></div>
|
|
{{ field }}
|
|
{% if field.help_text %}
|
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
|
{% endif %}
|
|
{% if field.errors %}
|
|
<div class="alert alert-danger mt-2 mb-0 py-2" role="alert">
|
|
{% for error in field.errors %}
|
|
<div><i class="bi bi-exclamation-circle"></i> {{ error }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<hr>
|
|
</div>
|
|
{% elif field.name == 'categories' %}
|
|
<div class="col-12 mb-3 {% if not field.value and not field.errors %}d-none{% endif %} {% if field.errors %}has-error{% endif %}" id="categories-container">
|
|
<hr>
|
|
<label class="form-label">{{ field.label }}</label>
|
|
<div class="row g-2" id="categories-grid">
|
|
{% for choice_value, choice_label in field.field.choices %}
|
|
<div class="col-md-3 col-sm-6">
|
|
<div class="form-check">
|
|
<input
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
name="categories"
|
|
value="{{ choice_value }}"
|
|
id="category_{{ choice_value }}"
|
|
{% if choice_value in field.value %}checked{% endif %}
|
|
>
|
|
<label class="form-check-label" for="category_{{ choice_value }}">
|
|
{{ choice_label }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% if field.help_text %}
|
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
|
{% endif %}
|
|
{% if field.errors %}
|
|
<div class="alert alert-danger mt-2 mb-0 py-2" role="alert">
|
|
{% for error in field.errors %}
|
|
<div><i class="bi bi-exclamation-circle"></i> {{ error }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% elif field.name == 'phase' %}
|
|
<div class="col-12 mb-3 {% if not field.value and not field.errors %}d-none{% endif %} {% if field.errors %}has-error{% endif %}" id="phase-container">
|
|
<label class="form-label">{{ field.label }}</label>
|
|
{{ field }}
|
|
{% if field.help_text %}
|
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
|
{% endif %}
|
|
{% if field.errors %}
|
|
<div class="alert alert-danger mt-2 mb-0 py-2" role="alert">
|
|
{% for error in field.errors %}
|
|
<div><i class="bi bi-exclamation-circle"></i> {{ error }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% elif field.name not in 'intervention,phase,lon,lat,geojson' %}
|
|
<div class="col-12 mb-3 {% if field.errors %}has-error{% endif %}">
|
|
<label class="form-label">{{ field.label }}</label>
|
|
{% if not field.field.required %}
|
|
<div class="optional-field">
|
|
{% endif %}
|
|
{{ field }}
|
|
{% if not field.field.required %}
|
|
</div>
|
|
{% endif %}
|
|
{% if field.help_text %}
|
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
|
{% endif %}
|
|
{% if field.errors %}
|
|
<div class="alert alert-danger mt-2 mb-0 py-2" role="alert">
|
|
{% for error in field.errors %}
|
|
<div><i class="bi bi-exclamation-circle"></i> {{ error }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Section Carte Géographique -->
|
|
<div class="row mt-4 mb-3">
|
|
<div class="col-12">
|
|
<hr>
|
|
<h5 class="mb-3">{% translate "Position géographique" %} <small class="text-muted">({% translate "facultatif" %})</small></h5>
|
|
|
|
<!-- Contrôles de la carte -->
|
|
<div class="mb-3">
|
|
<div class="d-flex gap-2 mb-3 flex-wrap">
|
|
<div class="flex-grow-1 position-relative">
|
|
<input type="text" id="address-search" class="form-control" placeholder="{% translate 'Rechercher une adresse...' %}" autocomplete="off">
|
|
<div id="address-results" class="list-group position-absolute w-100 shadow-sm" style="z-index: 1030; max-height: 280px; overflow:auto; display:none;"></div>
|
|
</div>
|
|
<button type="button" id="geolocation-btn" class="btn btn-outline-primary">
|
|
<i class="bi bi-geo-alt"></i> {% translate "Ma position" %}
|
|
</button>
|
|
<button type="button" id="clear-map-btn" class="btn btn-outline-danger">
|
|
<i class="bi bi-x-circle"></i> {% translate "Effacer" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Carte -->
|
|
<div id="map-control" style="height: 400px; border: 1px solid #dee2e6; border-radius: 0.375rem; margin-bottom: 1rem;"></div>
|
|
|
|
<!-- Champs cachés pour persister la position -->
|
|
{{ form.lon }}
|
|
{{ form.lat }}
|
|
{{ form.geojson }}
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary mb-2">
|
|
{% if control %}Mettre à jour{% else %}{% translate "Enregistrer" %}{% endif %}
|
|
</button>
|
|
<a href="{% if control %}{% url 'controls:controls_detail' control.id %}{% else %}{% url 'controls:controls_index' %}{% endif %}?v={{ request.GET.v }}"
|
|
class="btn btn-secondary mb-2">
|
|
{% translate "Annuler" %}
|
|
</a>
|
|
</form>
|
|
</div>
|
|
|
|
|
|
{% block script %}
|
|
<script src="{% static 'common/jquery.min.js' %}"></script>
|
|
<!-- MapLibre GL JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@3.3.1/dist/maplibre-gl.js"></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/maplibre-gl@3.3.1/dist/maplibre-gl.css" rel="stylesheet" />
|
|
|
|
<!-- MapTiler Geocoder -->
|
|
<script src="https://cdn.jsdelivr.net/npm/@maptiler/sdk@latest/dist/maptiler-sdk.umd.js"></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/@maptiler/sdk@latest/dist/maptiler-sdk.css" rel="stylesheet" />
|
|
|
|
<script>
|
|
// Initialisation de la carte
|
|
let map = null;
|
|
let marker = null;
|
|
const apiKey = (function(styleUrl) { if (!styleUrl) return ''; try { const url = new URL(styleUrl); return url.searchParams.get('key') || ''; } catch(e) { return ''; } })(window.MAP_STYLE_LIGHT);
|
|
const AUTOCOMPLETE_URL = "{% url 'common:autocomplete_street' %}";
|
|
|
|
function initializeMap() {
|
|
if (map) return;
|
|
|
|
const mapContainer = document.getElementById('map-control');
|
|
|
|
// Coordonnées par défaut (Bruxelles)
|
|
let center = [4.3572, 50.8477];
|
|
let zoom = 11;
|
|
|
|
// Si des coordonnées existent déjà, les utiliser
|
|
const lonInput = document.getElementById('id_lon');
|
|
const latInput = document.getElementById('id_lat');
|
|
|
|
if (lonInput && lonInput.value && latInput && latInput.value) {
|
|
center = [parseFloat(lonInput.value), parseFloat(latInput.value)];
|
|
zoom = 17;
|
|
}
|
|
|
|
map = new maplibregl.Map({
|
|
container: mapContainer,
|
|
style: window.MAP_STYLE_LIGHT,
|
|
center: center,
|
|
zoom: zoom,
|
|
minZoom: 9,
|
|
maxZoom: 24
|
|
});
|
|
|
|
map.on('load', () => {
|
|
// Ajouter le marker s'il y a des coordonnées existantes
|
|
if (lonInput && latInput && lonInput.value && latInput.value) {
|
|
addMarker(parseFloat(lonInput.value), parseFloat(latInput.value));
|
|
}
|
|
});
|
|
|
|
// Clic sur la carte pour placer le marker
|
|
map.on('click', (e) => {
|
|
addMarker(e.lngLat.lng, e.lngLat.lat);
|
|
});
|
|
}
|
|
|
|
function addMarker(lon, lat) {
|
|
if (marker) {
|
|
marker.remove();
|
|
}
|
|
|
|
marker = new maplibregl.Marker({ draggable: true, color: '#007bff' })
|
|
.setLngLat([lon, lat])
|
|
.addTo(map);
|
|
|
|
// Événement de fin de déplacement du marker
|
|
marker.on('dragend', () => {
|
|
const lngLat = marker.getLngLat();
|
|
updateCoordinates(lngLat.lng, lngLat.lat);
|
|
});
|
|
|
|
updateCoordinates(lon, lat);
|
|
|
|
// Centrer la carte sur le marker
|
|
map.flyTo({
|
|
center: [lon, lat],
|
|
zoom: 17,
|
|
duration: 1000
|
|
});
|
|
}
|
|
|
|
function updateCoordinates(lon, lat) {
|
|
const lonInput = document.getElementById('id_lon');
|
|
const latInput = document.getElementById('id_lat');
|
|
const geojsonInput = document.getElementById('id_geojson');
|
|
|
|
if (lonInput) lonInput.value = lon.toFixed(6);
|
|
if (latInput) latInput.value = lat.toFixed(6);
|
|
|
|
// Créer et sauvegarder le GeoJSON
|
|
const geojson = {
|
|
type: 'FeatureCollection',
|
|
features: [{
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [lon, lat]
|
|
}
|
|
}]
|
|
};
|
|
|
|
if (geojsonInput) {
|
|
geojsonInput.value = JSON.stringify(geojson);
|
|
}
|
|
|
|
}
|
|
|
|
function clearMap() {
|
|
if (marker) {
|
|
marker.remove();
|
|
marker = null;
|
|
}
|
|
|
|
const lonInput = document.getElementById('id_lon');
|
|
const latInput = document.getElementById('id_lat');
|
|
const geojsonInput = document.getElementById('id_geojson');
|
|
|
|
if (lonInput) lonInput.value = '';
|
|
if (latInput) latInput.value = '';
|
|
if (geojsonInput) geojsonInput.value = '';
|
|
|
|
// Réinitialiser le zoom à Bruxelles
|
|
map.flyTo({
|
|
center: [4.3572, 50.8477],
|
|
zoom: 11,
|
|
duration: 1000
|
|
});
|
|
}
|
|
|
|
// Géocodage avec MapTiler
|
|
async function geocodeAddress(address) {
|
|
try {
|
|
const response = await fetch(
|
|
`https://api.maptiler.com/geocoding/${encodeURIComponent(address)}.json?key=${apiKey}&country=be`
|
|
);
|
|
const data = await response.json();
|
|
|
|
if (data.features && data.features.length > 0) {
|
|
const feature = data.features[0];
|
|
const [lon, lat] = feature.geometry.coordinates;
|
|
|
|
addMarker(lon, lat);
|
|
return true;
|
|
} else {
|
|
alert('{% translate "Aucun résultat trouvé pour cette adresse" %}');
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur de géocodage:', error);
|
|
alert("{% translate "Erreur lors de la recherche d\'adresse" %}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Géolocalisation du navigateur
|
|
function requestGeolocation() {
|
|
if ('geolocation' in navigator) {
|
|
const btn = document.getElementById('geolocation-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> {% translate "Localisation..." %}';
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
const { longitude, latitude } = position.coords;
|
|
addMarker(longitude, latitude);
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-geo-alt"></i> {% translate "Ma position" %}';
|
|
},
|
|
(error) => {
|
|
console.warn('Erreur de géolocalisation:', error);
|
|
alert("{% translate "Impossible d\'accéder à votre position. Assurez-vous que vous avez autorisé l\'accès à la localisation." %}");
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-geo-alt"></i> {% translate "Ma position" %}';
|
|
},
|
|
{
|
|
enableHighAccuracy: true,
|
|
timeout: 10000,
|
|
maximumAge: 0
|
|
}
|
|
);
|
|
} else {
|
|
alert("{% translate "La géolocalisation n\'est pas disponible dans votre navigateur" %}");
|
|
}
|
|
}
|
|
|
|
// Initialisation à la fin du chargement du DOM
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialiser la carte
|
|
setTimeout(initializeMap, 100);
|
|
|
|
// Événements des boutons
|
|
const geolocationBtn = document.getElementById('geolocation-btn');
|
|
if (geolocationBtn) {
|
|
geolocationBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
requestGeolocation();
|
|
});
|
|
}
|
|
|
|
const clearBtn = document.getElementById('clear-map-btn');
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
clearMap();
|
|
});
|
|
}
|
|
|
|
// Recherche d'adresse avec autocomplétion (endpoint interne)
|
|
const addressSearch = document.getElementById('address-search');
|
|
const addressResults = document.getElementById('address-results');
|
|
let addrTimer = null;
|
|
function hideResultsSoon() {
|
|
setTimeout(() => { if (addressResults) addressResults.style.display = 'none'; }, 150);
|
|
}
|
|
if (addressSearch && addressResults) {
|
|
addressSearch.addEventListener('input', () => {
|
|
clearTimeout(addrTimer);
|
|
const q = addressSearch.value.trim();
|
|
if (q.length < 2) { addressResults.style.display = 'none'; addressResults.innerHTML = ''; return; }
|
|
addrTimer = setTimeout(async () => {
|
|
try {
|
|
const resp = await fetch(`${AUTOCOMPLETE_URL}?q=${encodeURIComponent(q)}`);
|
|
if (!resp.ok) { addressResults.style.display = 'none'; return; }
|
|
const items = await resp.json();
|
|
if (!Array.isArray(items) || items.length === 0) { addressResults.style.display = 'none'; addressResults.innerHTML = ''; return; }
|
|
addressResults.innerHTML = items.map(it => (
|
|
`<button type="button" class="list-group-item list-group-item-action" data-lon="${it.lon}" data-lat="${it.lat}" data-label="${(it.label || '').replace(/\"/g,'"')}">${it.label}</button>`
|
|
)).join('');
|
|
addressResults.style.display = 'block';
|
|
} catch (e) {
|
|
addressResults.style.display = 'none';
|
|
}
|
|
}, 250);
|
|
});
|
|
addressResults.addEventListener('click', (ev) => {
|
|
const btn = ev.target.closest('.list-group-item');
|
|
if (!btn) return;
|
|
const lon = parseFloat(btn.getAttribute('data-lon'));
|
|
const lat = parseFloat(btn.getAttribute('data-lat'));
|
|
const label = btn.getAttribute('data-label') || '';
|
|
const addressInput = document.getElementById('id_address');
|
|
if (!isNaN(lon) && !isNaN(lat)) { addMarker(lon, lat); }
|
|
if (addressInput) addressInput.value = label;
|
|
addressResults.style.display = 'none';
|
|
});
|
|
addressSearch.addEventListener('blur', hideResultsSoon);
|
|
}
|
|
});
|
|
|
|
// Gestion des thématiques en badges
|
|
document.querySelectorAll('.thematic-badge').forEach(badge => {
|
|
badge.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const checkbox = this.querySelector('input[type="checkbox"]');
|
|
|
|
// Toggle l'état de la checkbox
|
|
checkbox.checked = !checkbox.checked;
|
|
|
|
// Synchroniser la classe selected avec l'état de la checkbox
|
|
if (checkbox.checked) {
|
|
this.classList.add('selected');
|
|
} else {
|
|
this.classList.remove('selected');
|
|
}
|
|
|
|
// Forcer le rechargement des catégories car l'utilisateur a changé les thématiques
|
|
isInitialLoad = false;
|
|
updateCategories(true);
|
|
});
|
|
|
|
// Empêcher le comportement par défaut de la checkbox
|
|
const checkbox = badge.querySelector('input[type="checkbox"]');
|
|
if (checkbox) {
|
|
checkbox.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Gestion de l'autocomplétion des interventions
|
|
const interventionsSearch = document.getElementById('interventions-search');
|
|
const autocompleteResults = document.getElementById('autocomplete-results');
|
|
const interventionsList = document.getElementById('interventions-list');
|
|
const interventionsSelect = document.getElementById('id_interventions');
|
|
const selectedInterventions = [];
|
|
|
|
// Flag pour savoir si c'est un retour POST avec erreur
|
|
const isPostWithErrors = {{ is_post_with_errors|yesno:"true,false" }};
|
|
|
|
// Nettoyage forcé des inputs au chargement de la page (seulement si ce n'est pas un retour POST avec erreur)
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Si c'est un retour POST avec erreur, ne pas nettoyer les valeurs
|
|
if (isPostWithErrors) {
|
|
// Ne nettoyer que le champ de recherche d'interventions
|
|
if (interventionsSearch) {
|
|
interventionsSearch.value = '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Réinitialiser le champ de recherche d'interventions
|
|
if (interventionsSearch) {
|
|
interventionsSearch.value = '';
|
|
}
|
|
|
|
// Réinitialiser tous les inputs text et textarea du formulaire (sauf les hidden)
|
|
const formInputs = document.querySelectorAll('#control-form input[type="text"]:not([type="hidden"]), #control-form textarea');
|
|
formInputs.forEach(input => {
|
|
if (!input.hasAttribute('data-keep-value')) {
|
|
// Ne pas nettoyer les champs qui ont une valeur initiale du serveur
|
|
const fieldName = input.name;
|
|
if (!fieldName || !['control_code', 'location', 'description'].includes(fieldName)) {
|
|
input.value = '';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Pré-remplir avec les interventions déjà sélectionnées
|
|
const preselectedInterventions = {{ preselected_interventions|safe }};
|
|
if (preselectedInterventions && Array.isArray(preselectedInterventions)) {
|
|
preselectedInterventions.forEach(inter => {
|
|
if (!selectedInterventions.find(i => i.id === inter.id)) {
|
|
selectedInterventions.push(inter);
|
|
}
|
|
});
|
|
renderInterventions();
|
|
}
|
|
|
|
let searchTimeout;
|
|
interventionsSearch.addEventListener('input', function() {
|
|
clearTimeout(searchTimeout);
|
|
const query = this.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
autocompleteResults.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
searchTimeout = setTimeout(() => {
|
|
fetch(`{% url 'controls:interventions_autocomplete' %}?q=${encodeURIComponent(query)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
displayAutocompleteResults(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur autocomplétion:', error);
|
|
});
|
|
}, 300);
|
|
});
|
|
|
|
function displayAutocompleteResults(results) {
|
|
if (results.length === 0) {
|
|
autocompleteResults.innerHTML = '<div class="autocomplete-item">{% translate "Aucun résultat" %}</div>';
|
|
autocompleteResults.classList.add('show');
|
|
return;
|
|
}
|
|
|
|
autocompleteResults.innerHTML = results.map(inter => {
|
|
// Ne pas afficher les interventions déjà sélectionnées
|
|
if (selectedInterventions.find(i => i.id === inter.id)) {
|
|
return '';
|
|
}
|
|
return `
|
|
<div class="autocomplete-item" onclick="selectIntervention(${inter.id}, '${escapeHtml(inter.code)}', '${escapeHtml(inter.title)}', '${escapeHtml(inter.label)}', '${escapeHtml(inter.status)}', '${escapeHtml(inter.status_color)}')">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="autocomplete-item-code">${escapeHtml(inter.code)}</div>
|
|
<div class="autocomplete-item-title">${escapeHtml(inter.title)}</div>
|
|
</div>
|
|
<span class="badge bg-${escapeHtml(inter.status_color)} ms-2">${escapeHtml(inter.status)}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
autocompleteResults.classList.add('show');
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
window.selectIntervention = function(id, code, title, label, status, statusColor) {
|
|
if (!selectedInterventions.find(i => i.id === id)) {
|
|
selectedInterventions.push({id, code, title, label, status, status_color: statusColor});
|
|
renderInterventions();
|
|
}
|
|
interventionsSearch.value = '';
|
|
autocompleteResults.classList.remove('show');
|
|
};
|
|
|
|
function removeIntervention(id) {
|
|
const index = selectedInterventions.findIndex(i => i.id === id);
|
|
if (index > -1) {
|
|
selectedInterventions.splice(index, 1);
|
|
renderInterventions();
|
|
}
|
|
}
|
|
|
|
function renderInterventions() {
|
|
interventionsList.innerHTML = selectedInterventions.map(inter => `
|
|
<div class="intervention-badge">
|
|
<span>${escapeHtml(inter.code)}</span>
|
|
${inter.status && inter.status_color ? `<span class="badge bg-${escapeHtml(inter.status_color)} ms-1" style="font-size: 0.7rem;">${escapeHtml(inter.status)}</span>` : ''}
|
|
<button type="button" class="remove-btn" onclick="removeIntervention(${inter.id})" title="{% translate 'Retirer' %}">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Mettre à jour le select multiple
|
|
if (interventionsSelect) {
|
|
// Vider toutes les options
|
|
interventionsSelect.innerHTML = '';
|
|
// Ajouter les interventions sélectionnées
|
|
selectedInterventions.forEach(inter => {
|
|
const option = document.createElement('option');
|
|
option.value = inter.id;
|
|
option.selected = true;
|
|
interventionsSelect.appendChild(option);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Cacher les résultats quand on clique ailleurs
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.interventions-autocomplete-container')) {
|
|
autocompleteResults.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Gestion du projet et des phases
|
|
$(document).ready(function() {
|
|
$("#project-select").change(function() {
|
|
let projectId = $(this).val();
|
|
let phaseSelect = $("#phase-select");
|
|
let phaseContainer = $("#phase-container");
|
|
|
|
if (projectId) {
|
|
$.ajax({
|
|
url: "{% url 'projects:projects_phases' project_id=0 %}".replace("0", projectId.toString()),
|
|
data: { },
|
|
type: "GET",
|
|
success: function(data) {
|
|
phaseSelect.empty();
|
|
if (data.length > 0) {
|
|
phaseSelect.append('<option value="">Sélectionnez une phase</option>');
|
|
$.each(data, function(index, phase) {
|
|
phaseSelect.append('<option value="' + phase.id + '">' + phase.name + '</option>');
|
|
});
|
|
phaseContainer.removeClass("d-none");
|
|
} else {
|
|
phaseContainer.addClass("d-none");
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
phaseSelect.empty();
|
|
phaseContainer.addClass("d-none");
|
|
}
|
|
});
|
|
});
|
|
|
|
// Flag pour savoir si c'est le chargement initial de la page
|
|
let isInitialLoad = true;
|
|
|
|
// Fonction pour mettre à jour les catégories en fonction des thématiques sélectionnées
|
|
function updateCategories(forceReload = false) {
|
|
const thematicCheckboxes = document.querySelectorAll('input[name="thematics"]:checked');
|
|
const selected = Array.from(thematicCheckboxes).map(cb => cb.value);
|
|
const categoriesGrid = document.querySelector("#categories-grid");
|
|
const categoriesContainer = document.querySelector("#categories-container");
|
|
|
|
if (!categoriesGrid || selected.length === 0) {
|
|
if (categoriesContainer) {
|
|
categoriesContainer.classList.add("d-none");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Si c'est le chargement initial et que les catégories ont déjà été rendues par le serveur
|
|
// (retour POST avec erreur), ne pas les recharger via AJAX
|
|
const existingCategories = categoriesGrid.querySelectorAll('input[name="categories"]');
|
|
if (isInitialLoad && existingCategories.length > 0 && !forceReload) {
|
|
// Les catégories existent déjà, juste afficher le conteneur
|
|
categoriesContainer.classList.remove("d-none");
|
|
return;
|
|
}
|
|
|
|
fetch("{% url 'common:get_thematic_categories' %}?thematic_ids=" + selected.join(","))
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Récupérer les sélections existantes (au cas où)
|
|
const existingSelections = Array.from(document.querySelectorAll('input[name="categories"]:checked')).map(cb => cb.value);
|
|
|
|
categoriesGrid.innerHTML = ""; // reset
|
|
|
|
data.results.forEach(cat => {
|
|
const colDiv = document.createElement("div");
|
|
colDiv.className = "col-md-3 col-sm-6";
|
|
|
|
const checkDiv = document.createElement("div");
|
|
checkDiv.className = "form-check";
|
|
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.name = "categories";
|
|
checkbox.value = cat.id;
|
|
checkbox.id = "id_categories_" + cat.id;
|
|
checkbox.className = "form-check-input";
|
|
|
|
if (existingSelections.includes(cat.id.toString())) {
|
|
checkbox.checked = true;
|
|
}
|
|
|
|
const label = document.createElement("label");
|
|
label.className = "form-check-label";
|
|
label.htmlFor = checkbox.id;
|
|
label.textContent = cat.text;
|
|
|
|
checkDiv.appendChild(checkbox);
|
|
checkDiv.appendChild(label);
|
|
colDiv.appendChild(checkDiv);
|
|
categoriesGrid.appendChild(colDiv);
|
|
});
|
|
|
|
categoriesContainer.classList.remove("d-none");
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur lors du chargement des catégories:', error);
|
|
});
|
|
}
|
|
|
|
// Appel initial si des thématiques sont déjà sélectionnées
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const checkedThematics = document.querySelectorAll('input[name="thematics"]:checked');
|
|
if (checkedThematics.length > 0) {
|
|
updateCategories();
|
|
}
|
|
// Après le chargement initial, désactiver le flag
|
|
isInitialLoad = false;
|
|
});
|
|
</script>
|
|
{% endblock %} |