loko/streetup/controls/templates/controls/controls_form_content.html
2026-07-22 14:48:40 +02:00

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,'&quot;')}">${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 %}