1027 lines
No EOL
55 KiB
HTML
1027 lines
No EOL
55 KiB
HTML
{% extends "base.html" %}
|
||
{% load static %}
|
||
{% load i18n %}
|
||
|
||
{% block head %}
|
||
<link rel="stylesheet" href="{% static 'interventions/calendar/interventions_calendar.css' %}">
|
||
<link href="{% static 'common/mapbox-gl-draw.css' %}" rel="stylesheet"/>
|
||
<style>
|
||
.filter-section {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
/* Default badge styling */
|
||
.filter-option label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
background-color: #e7f1ff;
|
||
color: #0c5cc9;
|
||
border-radius: 1rem;
|
||
padding: 0.4rem 0.5rem;
|
||
font-size: 0.75rem;
|
||
border: 1px solid #b8d4f1;
|
||
transition: all 0.2s ease;
|
||
width: 140px;
|
||
flex: 1 1 auto;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* Contextual color badges - inactive state (outline) */
|
||
.filter-option label.context-badge {
|
||
border-color: currentColor;
|
||
color: inherit;
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* Bootstrap outline color mappings */
|
||
.filter-option label.btn-outline-primary {
|
||
border-color: #0c5cc9;
|
||
color: #0c5cc9;
|
||
}
|
||
.filter-option label.btn-outline-danger {
|
||
border-color: #dc3545;
|
||
color: #dc3545;
|
||
}
|
||
.filter-option label.btn-outline-warning {
|
||
border-color: #ffc107;
|
||
color: #ffc107;
|
||
}
|
||
.filter-option label.btn-outline-success {
|
||
border-color: #28a745;
|
||
color: #28a745;
|
||
}
|
||
.filter-option label.btn-outline-info {
|
||
border-color: #17a2b8;
|
||
color: #17a2b8;
|
||
}
|
||
.filter-option label.btn-outline-dark {
|
||
border-color: #343a40;
|
||
color: #343a40;
|
||
}
|
||
.filter-option label.btn-outline-secondary {
|
||
border-color: #6c757d;
|
||
color: #6c757d;
|
||
}
|
||
|
||
/* Default badges - checked state */
|
||
.filter-option input[type="checkbox"]:checked + label:not(.context-badge) {
|
||
background-color: #0c5cc9;
|
||
color: white;
|
||
border-color: #0c5cc9;
|
||
}
|
||
|
||
/* Contextual badges - checked state (filled/enfoncé) */
|
||
.filter-option input[type="checkbox"]:checked + label.context-badge {
|
||
color: white;
|
||
}
|
||
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-primary {
|
||
background-color: #0c5cc9;
|
||
border-color: #0c5cc9;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-danger {
|
||
background-color: #dc3545;
|
||
border-color: #dc3545;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-warning {
|
||
background-color: #ffc107;
|
||
border-color: #ffc107;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-success {
|
||
background-color: #28a745;
|
||
border-color: #28a745;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-info {
|
||
background-color: #17a2b8;
|
||
border-color: #17a2b8;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-dark {
|
||
background-color: #343a40;
|
||
border-color: #343a40;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-secondary {
|
||
background-color: #6c757d;
|
||
border-color: #6c757d;
|
||
}
|
||
|
||
/* Default badges - hover */
|
||
.filter-option input[type="checkbox"]:hover + label:not(.context-badge) {
|
||
background-color: #cfe2f3;
|
||
}
|
||
|
||
/* Contextual badges - hover */
|
||
.filter-option input[type="checkbox"]:hover + label.context-badge {
|
||
background-color: inherit;
|
||
}
|
||
|
||
.filter-section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.75rem;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.filter-section-header label {
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin: 0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.filter-section-buttons {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.filter-section-buttons button {
|
||
padding: 0.25rem 0.5rem;
|
||
font-size: 0.75rem;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
min-height: 2.5rem;
|
||
padding: 0.5rem;
|
||
background-color: #f8f9fa;
|
||
border-radius: 0.375rem;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.filter-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.filter-option input[type="checkbox"] {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Priority gradients & custom styles */
|
||
.filter-option label.btn-outline-priority-1 {
|
||
border-color: #dc3545;
|
||
color: #dc3545;
|
||
}
|
||
.filter-option label.btn-outline-priority-2 {
|
||
border-color: #fd7e14;
|
||
color: #fd7e14;
|
||
}
|
||
.filter-option label.btn-outline-priority-3 {
|
||
border-color: #0d6efd;
|
||
color: #0d6efd;
|
||
}
|
||
.filter-option label.btn-outline-priority-4 {
|
||
border-color: #6c757d;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.filter-option label[class*="btn-outline-priority-"] {
|
||
transition: all 0.2s ease-in-out;
|
||
}
|
||
|
||
.filter-option input[type="checkbox"]:hover + label[class*="btn-outline-priority-"] {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.08);
|
||
}
|
||
|
||
.filter-option input[type="checkbox"]:hover + label.btn-outline-priority-1:not(:checked) {
|
||
background-color: rgba(220, 53, 69, 0.05);
|
||
}
|
||
.filter-option input[type="checkbox"]:hover + label.btn-outline-priority-2:not(:checked) {
|
||
background-color: rgba(253, 126, 20, 0.05);
|
||
}
|
||
.filter-option input[type="checkbox"]:hover + label.btn-outline-priority-3:not(:checked) {
|
||
background-color: rgba(13, 110, 253, 0.05);
|
||
}
|
||
.filter-option input[type="checkbox"]:hover + label.btn-outline-priority-4:not(:checked) {
|
||
background-color: rgba(108, 117, 125, 0.05);
|
||
}
|
||
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-priority-1 {
|
||
background: linear-gradient(135deg, #dc3545 0%, #ff5e62 100%) !important;
|
||
border-color: #dc3545 !important;
|
||
color: white !important;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-priority-2 {
|
||
background: linear-gradient(135deg, #fd7e14 0%, #ffb703 100%) !important;
|
||
border-color: #fd7e14 !important;
|
||
color: white !important;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-priority-3 {
|
||
background: linear-gradient(135deg, #0d6efd 0%, #00b4d8 100%) !important;
|
||
border-color: #0d6efd !important;
|
||
color: white !important;
|
||
}
|
||
.filter-option input[type="checkbox"]:checked + label.btn-outline-priority-4 {
|
||
background: linear-gradient(135deg, #6c757d 0%, #adb5bd 100%) !important;
|
||
border-color: #6c757d !important;
|
||
color: white !important;
|
||
}
|
||
</style>
|
||
{% endblock head %}
|
||
|
||
{% block content %}
|
||
<form method="get" id="calendar-filters-form" class="calendar-toolbar" aria-label="{% translate 'Filtres du calendrier' %}">
|
||
<div class="calendar-toolbar__main" id="calendar-toolbar">
|
||
<div class="calendar-toolbar__field">
|
||
<label for="filter-view-mode" class="form-label">{% translate "Mode d'affichage" %}</label>
|
||
<select name="view_mode" id="filter-view-mode" class="form-select form-select-sm">
|
||
<option value="timeline" {% if selected_view_mode == 'timeline' %}selected{% endif %}>{% translate "Chronologie" %}</option>
|
||
{% if can_use_member_view %}
|
||
<option value="members" {% if selected_view_mode == 'members' %}selected{% endif %}>{% translate "Par membre assigné" %}</option>
|
||
<option value="teams_daily" {% if selected_view_mode == 'teams_daily' %}selected{% endif %}>{% translate "Par équipe et par jour" %}</option>
|
||
{% endif %}
|
||
</select>
|
||
</div>
|
||
<div class="calendar-toolbar__views" id="timeline-view-buttons">
|
||
<button class="btn btn-light btn-sm" data-timeline-view="hour" type="button">{% translate "Heure" %}</button>
|
||
<button class="btn btn-light btn-sm" data-timeline-view="day" type="button">{% translate "Jour" %}</button>
|
||
<button class="btn btn-light btn-sm" data-timeline-view="week" type="button">{% translate "Semaine" %}</button>
|
||
<button class="btn btn-light btn-sm" data-timeline-view="month" type="button">{% translate "Mois" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="calendar-toolbar__actions">
|
||
<div class="input-group input-group-sm me-2" style="width: 380px;">
|
||
<input type="text" name="q" id="calendar-search-input" class="form-control" placeholder="{% translate 'Titre, code, localisation, contrat…' %}" value="{{ selected_search|default:'' }}">
|
||
<button class="btn btn-outline-secondary" type="button" id="calendar-search-clear" title="{% translate 'Effacer la recherche' %}" {% if not selected_search %}style="display:none;"{% endif %}>
|
||
<i class="bi bi-x-lg"></i>
|
||
</button>
|
||
<button class="btn btn-outline-secondary" type="submit" id="calendar-search-btn">
|
||
<i class="bi bi-search"></i>
|
||
</button>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-secondary calendar-toolbar__map-btn me-1 position-relative" data-bs-toggle="modal" data-bs-target="#calendarMapModal" title="{% translate 'Carte des interventions' %}">
|
||
<i class="bi bi-map"></i> {% translate "Carte" %}
|
||
{% if selected_bbox or selected_poly %}
|
||
<span class="position-absolute top-0 start-100 translate-middle p-1 bg-danger border border-light rounded-circle"
|
||
title="{% translate 'Filtre géographique actif' %}"><span class="visually-hidden">{% translate "Filtre géographique actif" %}</span></span>
|
||
{% endif %}
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary calendar-toolbar__filters-btn" data-bs-toggle="modal" data-bs-target="#calendarFiltersModal">
|
||
<i class="bi bi-funnel"></i> {% translate "Filtres" %}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="modal fade" id="calendarFiltersModal" tabindex="-1" aria-labelledby="calendarFiltersModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="calendarFiltersModalLabel">{% translate "Filtres" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="row">
|
||
<!-- Thématiques -->
|
||
<div class="col-12 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Thématiques" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="thematic">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="thematic">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="thematic">
|
||
{% for key, value, icon in thematic_choices %}
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="thematic" id="thematic_{{ key }}" value="{{ key }}" {% if key in selected_thematics %}checked{% endif %}>
|
||
<label for="thematic_{{ key }}">
|
||
{% if icon %}<i class="bi {{ icon }} me-2"></i>{% endif %}{{ value }}
|
||
</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Statuts -->
|
||
<div class="col-12 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Statuts" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="status">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="status">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="status">
|
||
{% for key, value, css_class in status_choices %}
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="status" id="status_{{ key }}" value="{{ key }}" {% if key in selected_statuses %}checked{% endif %}>
|
||
<label for="status_{{ key }}" class="context-badge status-{{ key }} btn-outline-{{ css_class }}">{{ value }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Catégorie -->
|
||
<div class="col-12 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Catégorie" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="asset_category">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="asset_category">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="asset_category">
|
||
{% for id, name, thematic_code in asset_category_choices %}
|
||
<div class="filter-option" data-thematic="{{ thematic_code }}">
|
||
<input type="checkbox" name="asset_category" id="asset_category_{{ id }}" value="{{ id }}" {% if id in selected_categories %}checked{% endif %}>
|
||
<label for="asset_category_{{ id }}">{{ name }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Autres filtres en deux colonnes -->
|
||
<div class="row mt-3">
|
||
<!-- Type de maintenance -->
|
||
<div class="col-12 col-md-6 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Type de maintenance" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="maintain_type">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="maintain_type">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="maintain_type">
|
||
{% for key, value, css_class in maintain_type_choices %}
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="maintain_type" id="maintain_type_{{ key }}" value="{{ key }}" {% if key in selected_maintain_types %}checked{% endif %}>
|
||
<label for="maintain_type_{{ key }}" class="context-badge btn-outline-{{ css_class }}">{{ value }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contrat -->
|
||
<div class="col-12 col-md-6 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Contrat" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="contract">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="contract">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="contract">
|
||
{% for id, number, thematic_codes in contract_choices %}
|
||
<div class="filter-option" data-thematics="{{ thematic_codes|join:',' }}">
|
||
<input type="checkbox" name="contract" id="contract_{{ id }}" value="{{ id }}" {% if id in selected_contracts %}checked{% endif %}>
|
||
<label for="contract_{{ id }}">{{ number }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bon de préparation -->
|
||
<div class="col-12 col-md-6 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Bon de préparation" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="preparation_state">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="preparation_state">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="preparation_state">
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="preparation_state" id="preparation_state_processed" value="processed" {% if "processed" in selected_preparation_states %}checked{% endif %}>
|
||
<label for="preparation_state_processed" class="context-badge btn-outline-success">
|
||
<i class="bi bi-check-circle me-1"></i>{% translate "Prêt" %}
|
||
</label>
|
||
</div>
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="preparation_state" id="preparation_state_not_processed" value="not_processed" {% if "not_processed" in selected_preparation_states %}checked{% endif %}>
|
||
<label for="preparation_state_not_processed" class="context-badge btn-outline-danger">
|
||
<i class="bi bi-hourglass me-1"></i>{% translate "En attente" %}
|
||
</label>
|
||
</div>
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="preparation_state" id="preparation_state_none" value="none" {% if "none" in selected_preparation_states %}checked{% endif %}>
|
||
<label for="preparation_state_none" class="context-badge btn-outline-secondary">
|
||
<i class="bi bi-question-circle me-1"></i>{% translate "Aucun bon" %}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Priorité -->
|
||
<div class="col-12 col-md-6 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Priorité" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="priority">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="priority">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="priority">
|
||
{% for key, value in priority_choices %}
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="priority" id="priority_{{ key }}" value="{{ key }}" {% if key in selected_priorities %}checked{% endif %}>
|
||
<label for="priority_{{ key }}" class="context-badge btn-outline-priority-{{ key }}">{{ value }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<a href="{% url 'interventions:interventions_calendar' %}?view_mode={{ selected_view_mode }}" id="reset-filters" class="btn btn-outline-secondary">{% translate "Réinitialiser" %}</a>
|
||
<button type="submit" class="btn btn-primary">{% translate "Appliquer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# Champs cachés pour le filtre géographique #}
|
||
<input type="hidden" name="bbox" id="hidden-geo-bbox" value="{{ selected_bbox }}">
|
||
<input type="hidden" name="poly" id="hidden-geo-poly" value="{{ selected_poly }}">
|
||
|
||
{# Modale Carte #}
|
||
<div class="modal fade" id="calendarMapModal" tabindex="-1" aria-labelledby="calendarMapModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered" style="max-width:min(95vw,1400px);width:95vw;">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="calendarMapModalLabel">{% translate "Carte des interventions" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||
</div>
|
||
<div class="modal-body p-0" style="height:65vh;">
|
||
<div class="d-flex gap-3 align-items-center px-3 py-2 border-bottom bg-light flex-wrap">
|
||
<div class="btn-group btn-group-sm" role="group" aria-label="{% translate 'Mode de filtre géographique' %}">
|
||
<input type="radio" class="btn-check" name="geo-filter-mode" id="geo-mode-viewport" value="viewport" checked>
|
||
<label class="btn btn-outline-secondary" for="geo-mode-viewport">
|
||
<i class="bi bi-bounding-box"></i> {% translate "Vue courante" %}
|
||
</label>
|
||
<input type="radio" class="btn-check" name="geo-filter-mode" id="geo-mode-polygon" value="polygon">
|
||
<label class="btn btn-outline-secondary" for="geo-mode-polygon">
|
||
<i class="bi bi-pentagon"></i> {% translate "Dessiner un polygone" %}
|
||
</label>
|
||
</div>
|
||
<small class="text-muted fst-italic" id="geo-filter-hint">{% translate "Naviguez et cliquez Appliquer pour filtrer sur la zone visible." %}</small>
|
||
</div>
|
||
<div id="calendar-map" style="height:calc(65vh - 56px);"></div>
|
||
</div>
|
||
<div class="modal-footer justify-content-between">
|
||
<div class="d-flex gap-2">
|
||
<button type="button" class="btn btn-outline-danger btn-sm" id="geo-filter-clear">
|
||
<i class="bi bi-x-circle"></i> {% translate "Supprimer le filtre géographique" %}
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm" id="geo-team-toggle">
|
||
<i class="bi bi-people"></i> {% translate "Répartition par équipe" %}
|
||
</button>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||
<button type="button" class="btn btn-primary" id="geo-filter-apply">{% translate "Appliquer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Conteneur Timeline -->
|
||
<div id="timeline-wrapper" data-can-drag="{{ can_move_interventions|yesno:'true,false' }}" data-is-manager="{{ is_manager|yesno:'true,false' }}">
|
||
<div id="vis-timeline"></div>
|
||
</div>
|
||
|
||
<div id="members-view-layout" class="members-view-layout d-none">
|
||
<div id="members-timeline-wrapper" class="members-view-layout__timeline"></div>
|
||
<aside id="members-backlog-wrapper" class="members-view-layout__backlog"></aside>
|
||
</div>
|
||
|
||
<div id="teams-daily-view-layout" class="teams-daily-view-layout d-none">
|
||
<div class="teams-daily-main-content">
|
||
<div class="teams-daily-header-container bg-white border-bottom shadow-sm">
|
||
<div class="d-flex justify-content-between align-items-center p-3 flex-wrap gap-2">
|
||
<div class="d-flex gap-2 align-items-center">
|
||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn-teams-daily-prev">
|
||
<i class="bi bi-chevron-left"></i> {% translate "Précédent" %}
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn-teams-daily-today">
|
||
{% translate "Aujourd'hui" %}
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn-teams-daily-next">
|
||
{% translate "Suivant" %} <i class="bi bi-chevron-right"></i>
|
||
</button>
|
||
<span id="teams-daily-range-label" class="fw-bold ms-2 text-primary"></span>
|
||
</div>
|
||
<div>
|
||
<input type="date" id="teams-daily-datepicker" class="form-control form-control-sm" style="width: auto;">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="teams-daily-grid-container table-responsive">
|
||
<table class="table table-bordered teams-daily-table mb-0">
|
||
<thead>
|
||
<tr id="teams-daily-table-header">
|
||
<th class="bg-light sticky-left font-weight-bold text-center align-middle" style="min-width: 220px; width: 220px; z-index: 4;">
|
||
{% translate "Équipes / Jours" %}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="teams-daily-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<aside id="teams-daily-backlog-wrapper" class="teams-daily-view-layout__backlog"></aside>
|
||
</div>
|
||
|
||
<!-- <div id="calendar"></div> -->
|
||
|
||
<!-- Modal Bootstrap pour afficher les détails d'une intervention -->
|
||
<div class="modal fade" id="interventionModal" tabindex="-1" aria-labelledby="interventionModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="interventionModalLabel">{% translate "Planification de l’intervention" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<p class="mb-1">
|
||
<strong>{% translate "Code" %} :</strong> <span id="modal-intervention-code" class="text-primary"></span>
|
||
</p>
|
||
<p class="mb-1">
|
||
<strong>{% translate "Titre" %} :</strong> <span id="modal-intervention-title"></span>
|
||
</p>
|
||
<p class="mb-0">
|
||
<strong>{% translate "Statut" %} :</strong> <span id="modal-intervention-status" class="badge rounded-pill"></span>
|
||
</p>
|
||
<p class="mb-0">
|
||
<strong>{% translate "Bon de préparation" %} :</strong> <span id="modal-preparation-status"></span>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Dates sections -->
|
||
<div class="mb-3">
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th></th>
|
||
<th class="text-center">{% translate "Début" %}</th>
|
||
<th class="text-center">{% translate "Fin" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr id="modal-dates-expected-row">
|
||
<th scope="row" class="text-nowrap">{% translate "Prévision" %}</th>
|
||
<td class="text-center" id="modal-expected-begin">-</td>
|
||
<td class="text-center" id="modal-expected-end">-</td>
|
||
</tr>
|
||
<tr id="modal-dates-planned-row">
|
||
<th scope="row" class="text-nowrap">{% translate "Planification" %}</th>
|
||
<td class="text-center" id="modal-planned-begin">-</td>
|
||
<td class="text-center" id="modal-planned-end">-</td>
|
||
</tr>
|
||
<tr id="modal-dates-actual-row">
|
||
<th scope="row" class="text-nowrap">{% translate "Réalisation" %}</th>
|
||
<td class="text-center" id="modal-actual-begin">-</td>
|
||
<td class="text-center" id="modal-actual-end">-</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="planning-form" class="row g-3 mt-2">
|
||
<div class="col-12">
|
||
<h6 class="text-muted mb-2">{% translate "Modifier la planification" %}</h6>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label for="planning-begin-input" class="form-label">{% translate "Début planifié" %}</label>
|
||
<input type="datetime-local" class="form-control" id="planning-begin-input" required>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label for="planning-end-input" class="form-label">{% translate "Fin planifiée" %}</label>
|
||
<input type="datetime-local" class="form-control" id="planning-end-input" required>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label for="planning-duration-input" class="form-label">{% translate "Durée (heures)" %}</label>
|
||
<input type="number" class="form-control" id="planning-duration-input" min="0.25" step="0.25" inputmode="decimal">
|
||
</div>
|
||
<div class="row mt-3">
|
||
<div class="col-12 col-md-6">
|
||
<label for="planning-team-input" class="form-label">{% translate "Équipe assignée" %}</label>
|
||
<input type="text" class="form-control" id="planning-team-input" readonly disabled>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label for="planning-member-input" class="form-label">{% translate "Membre assigné" %}</label>
|
||
<input type="text" class="form-control" id="planning-member-input" readonly disabled>
|
||
</div>
|
||
</div>
|
||
<div class="col-12 d-none" id="planning-feedback"></div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<a id="modal-intervention-link" href="#" class="btn btn-outline-primary" target="_blank">{% translate "Voir l’intervention" %}</a>
|
||
<button type="button" class="btn btn-primary" id="planning-save-btn">{% translate "Enregistrer" %}</button>
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock content%}
|
||
|
||
{% block script %}
|
||
{{ calendar_preferences|json_script:"calendar-preferences" }}
|
||
<script>
|
||
const interventionseventsUrl = "{% url 'interventions:interventions_events' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode|escapejs }}{% endif %}";
|
||
const interventionScheduleUpdateUrl = "{% url 'interventions:interventions_schedule_update' %}";
|
||
const calendarGeojsonBaseUrl = "{% url 'interventions:interventions_geojson' %}";
|
||
const saveCalendarPreferencesUrl = "{% url 'common:save_calendar_preferences' %}";
|
||
</script>
|
||
<script src="{% url 'javascript-catalog' %}"></script>
|
||
<script src="{% static 'interventions/status_colors.js' %}"></script>
|
||
<script src="{% static 'interventions/calendar/interventions_calendar.js' %}"></script>
|
||
<script src="{% static 'common/turf.min.js' %}"></script>
|
||
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
||
<script>
|
||
(function () {
|
||
// Toggle checkboxes select all / deselect all
|
||
document.querySelectorAll('.select-all-btn').forEach(btn => {
|
||
btn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
const field = this.getAttribute('data-field');
|
||
const container = document.querySelector(`.filter-options[data-field="${field}"]`);
|
||
if (container) {
|
||
const checkboxes = container.querySelectorAll('input[type="checkbox"]:not(:disabled)');
|
||
checkboxes.forEach(checkbox => {
|
||
const parent = checkbox.closest('.filter-option');
|
||
if (!parent || parent.style.display !== 'none') {
|
||
checkbox.checked = true;
|
||
}
|
||
});
|
||
if (field === 'thematic') {
|
||
applyThematicFilters();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.deselect-all-btn').forEach(btn => {
|
||
btn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
const field = this.getAttribute('data-field');
|
||
const container = document.querySelector(`.filter-options[data-field="${field}"]`);
|
||
if (container) {
|
||
const checkboxes = container.querySelectorAll('input[type="checkbox"]:not(:disabled)');
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.checked = false;
|
||
});
|
||
if (field === 'thematic') {
|
||
applyThematicFilters();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
function getSelectedThematics() {
|
||
return Array.from(document.querySelectorAll('input[name="thematic"]:checked')).map(cb => cb.value);
|
||
}
|
||
|
||
function applyThematicFilters() {
|
||
const selected = getSelectedThematics();
|
||
const noFilter = selected.length === 0;
|
||
|
||
// Filter Categories
|
||
const categoryOptions = document.querySelectorAll('.filter-options[data-field="asset_category"] .filter-option');
|
||
categoryOptions.forEach(option => {
|
||
const thematic = option.getAttribute('data-thematic');
|
||
const visible = noFilter || selected.includes(thematic);
|
||
const checkbox = option.querySelector('input[type="checkbox"]');
|
||
if (!visible) {
|
||
option.style.display = 'none';
|
||
if (checkbox) checkbox.checked = false;
|
||
} else {
|
||
option.style.display = '';
|
||
}
|
||
});
|
||
|
||
// Filter Contracts
|
||
const contractOptions = document.querySelectorAll('.filter-options[data-field="contract"] .filter-option');
|
||
contractOptions.forEach(option => {
|
||
const thematics = (option.getAttribute('data-thematics') || '').split(',').filter(Boolean);
|
||
const visible = noFilter || thematics.some(t => selected.includes(t));
|
||
const checkbox = option.querySelector('input[type="checkbox"]');
|
||
if (!visible) {
|
||
option.style.display = 'none';
|
||
if (checkbox) checkbox.checked = false;
|
||
} else {
|
||
option.style.display = '';
|
||
}
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('input[name="thematic"]').forEach(cb => {
|
||
cb.addEventListener('change', applyThematicFilters);
|
||
});
|
||
|
||
// Apply on page load to respect pre-selected thematics
|
||
applyThematicFilters();
|
||
})();
|
||
|
||
// ---- Carte des interventions (modal) ----
|
||
(function () {
|
||
const MAPTILER_STYLE = window.MAP_STYLE_LIGHT;
|
||
const STATUS_COLOR_EXPR = [
|
||
'match', ['get', 'status'],
|
||
'in_preparation', '#6c757d',
|
||
'to_be_approved', '#ffc107',
|
||
'to_be_planned', '#ffc107',
|
||
'to_be_processed', '#dc3545',
|
||
'assigned', '#ffc107',
|
||
'in_progress', '#007bff',
|
||
'on_pause', '#ffc107',
|
||
'finished', '#17a2b8',
|
||
'processed', '#17a2b8',
|
||
'to_be_corrected', '#dc3545',
|
||
'corrected', '#17a2b8',
|
||
'validated', '#28a745',
|
||
'invoiced', '#343a40',
|
||
'closed', '#343a40',
|
||
'canceled', '#6c757d',
|
||
'#888888'
|
||
];
|
||
const DRAW_STYLES = [{id:'gl-draw-polygon-fill',type:'fill',filter:['all',['==','$type','Polygon']],paint:{'fill-color':['case',['==',['get','active'],'true'],'orange','#0066cc'],'fill-opacity':.15}},{id:'gl-draw-lines',type:'line',filter:['any',['==','$type','LineString'],['==','$type','Polygon']],layout:{'line-cap':'round','line-join':'round'},paint:{'line-color':['case',['==',['get','active'],'true'],'orange','#0066cc'],'line-width':2}},{id:'gl-draw-point-outer',type:'circle',filter:['all',['==','$type','Point'],['==','meta','feature']],paint:{'circle-radius':['case',['==',['get','active'],'true'],7,5],'circle-color':'white'}},{id:'gl-draw-point-inner',type:'circle',filter:['all',['==','$type','Point'],['==','meta','feature']],paint:{'circle-radius':['case',['==',['get','active'],'true'],5,3],'circle-color':['case',['==',['get','active'],'true'],'orange','#0066cc']}},{id:'gl-draw-vertex-outer',type:'circle',filter:['all',['==','$type','Point'],['==','meta','vertex'],['!=','mode','simple_select']],paint:{'circle-radius':['case',['==',['get','active'],'true'],7,5],'circle-color':'white'}},{id:'gl-draw-vertex-inner',type:'circle',filter:['all',['==','$type','Point'],['==','meta','vertex'],['!=','mode','simple_select']],paint:{'circle-radius':['case',['==',['get','active'],'true'],5,3],'circle-color':'orange'}},{id:'gl-draw-midpoint',type:'circle',filter:['all',['==','meta','midpoint']],paint:{'circle-radius':3,'circle-color':'orange'}}];
|
||
|
||
const TEAM_PALETTE = ['#0066CC','#1A9850','#F46D43','#9E0142','#3288BD','#66C2A5','#FF8800','#D53E4F','#5E4FA2','#66BD63'];
|
||
const UNASSIGNED_COLOR = '#dc3545';
|
||
|
||
let calMap = null;
|
||
let calDraw = null;
|
||
let mapReady = false;
|
||
let calPopup = null;
|
||
let lastFeatureCollection = null;
|
||
let teamMode = false;
|
||
|
||
const modalEl = document.getElementById('calendarMapModal');
|
||
const filtersForm = document.getElementById('calendar-filters-form');
|
||
const bboxInput = document.getElementById('hidden-geo-bbox');
|
||
const polyInput = document.getElementById('hidden-geo-poly');
|
||
|
||
// Build geojson URL from current page params, converting asset_category → category, dropping geo params
|
||
function buildGeoJsonUrl() {
|
||
const raw = new URLSearchParams(window.location.search);
|
||
const p = new URLSearchParams();
|
||
for (const [k, v] of raw.entries()) {
|
||
if (k === 'asset_category') { p.append('category', v); }
|
||
else if (k === 'bbox' || k === 'poly') { /* strip */ }
|
||
else { p.append(k, v); }
|
||
}
|
||
const qs = p.toString();
|
||
return calendarGeojsonBaseUrl + (qs ? '?' + qs : '');
|
||
}
|
||
|
||
function getMode() {
|
||
return document.querySelector('input[name="geo-filter-mode"]:checked')?.value || 'viewport';
|
||
}
|
||
|
||
function updateModeUI() {
|
||
const polygon = getMode() === 'polygon';
|
||
const hint = document.getElementById('geo-filter-hint');
|
||
if (hint) {
|
||
hint.textContent = polygon
|
||
? gettext('Dessinez un polygone sur la carte, puis cliquez Appliquer.')
|
||
: gettext('Naviguez et cliquez Appliquer pour filtrer sur la zone visible.');
|
||
}
|
||
// Show/hide individual draw buttons
|
||
if (calMap) {
|
||
document.querySelectorAll('#calendar-map .mapbox-gl-draw_ctrl-draw-btn, #calendar-map .mapbox-gl-draw_trash')
|
||
.forEach(el => { el.style.display = polygon ? '' : 'none'; });
|
||
if (!polygon && calDraw) {
|
||
calDraw.deleteAll();
|
||
calDraw.changeMode('simple_select');
|
||
}
|
||
}
|
||
}
|
||
|
||
function buildTeamColorExpr() {
|
||
const features = lastFeatureCollection ? lastFeatureCollection.features : [];
|
||
const teams = [...new Set(features.map(f => (f.properties && f.properties.team) || '').filter(Boolean))].sort();
|
||
const expr = ['match', ['get', 'team']];
|
||
const legend = [{ color: UNASSIGNED_COLOR, label: gettext('Non assigné(e)') }];
|
||
teams.forEach(function (team, i) {
|
||
expr.push(team, TEAM_PALETTE[i % TEAM_PALETTE.length]);
|
||
legend.push({ color: TEAM_PALETTE[i % TEAM_PALETTE.length], label: team });
|
||
});
|
||
expr.push(UNASSIGNED_COLOR); // fallback = non assigné
|
||
return { expr: expr, legend: legend };
|
||
}
|
||
|
||
function renderLegend(legendData) {
|
||
var el = document.getElementById('cal-team-legend');
|
||
if (!legendData) { if (el) el.remove(); return; }
|
||
var mapEl = document.getElementById('calendar-map');
|
||
if (!mapEl) return;
|
||
if (!el) {
|
||
el = document.createElement('div');
|
||
el.id = 'cal-team-legend';
|
||
el.style.cssText = 'position:absolute;top:10px;left:10px;z-index:10;background:rgba(255,255,255,0.93);border-radius:6px;box-shadow:0 1px 6px rgba(0,0,0,.22);padding:8px 12px;min-width:140px;max-width:230px;font-size:12px;line-height:1.6;pointer-events:none;';
|
||
mapEl.appendChild(el);
|
||
}
|
||
el.innerHTML = '<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:#555;margin-bottom:4px;">' + gettext('Équipes') + '</div>' +
|
||
legendData.map(function (d) {
|
||
return '<div style="display:flex;align-items:center;gap:6px;">' +
|
||
'<span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:' + d.color + ';flex-shrink:0;"></span>' +
|
||
'<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + d.label + '</span></div>';
|
||
}).join('');
|
||
}
|
||
|
||
function applyTeamMode() {
|
||
if (!calMap || !mapReady || !calMap.getLayer('cal-ivt-circles')) return;
|
||
if (teamMode) {
|
||
var tc = buildTeamColorExpr();
|
||
calMap.setPaintProperty('cal-ivt-circles', 'circle-color', tc.expr);
|
||
renderLegend(tc.legend);
|
||
} else {
|
||
calMap.setPaintProperty('cal-ivt-circles', 'circle-color', STATUS_COLOR_EXPR);
|
||
renderLegend(null);
|
||
}
|
||
}
|
||
|
||
function loadMapData() {
|
||
if (!calMap || !mapReady) return;
|
||
fetch(buildGeoJsonUrl())
|
||
.then(r => r.json())
|
||
.then(fc => {
|
||
lastFeatureCollection = fc;
|
||
if (calMap.getSource('cal-ivt')) {
|
||
calMap.getSource('cal-ivt').setData(fc);
|
||
} else {
|
||
calMap.addSource('cal-ivt', { type: 'geojson', data: fc });
|
||
calMap.addLayer({
|
||
id: 'cal-ivt-circles',
|
||
type: 'circle',
|
||
source: 'cal-ivt',
|
||
paint: {
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 9, 4, 18, 14],
|
||
'circle-color': STATUS_COLOR_EXPR,
|
||
'circle-opacity': 0.85,
|
||
'circle-stroke-width': 1,
|
||
'circle-stroke-color': 'rgba(255,255,255,0.6)'
|
||
}
|
||
});
|
||
calMap.on('click', 'cal-ivt-circles', e => {
|
||
if (!e.features.length) return;
|
||
const props = e.features[0].properties;
|
||
if (calPopup) calPopup.remove();
|
||
calPopup = new maplibregl.Popup({ maxWidth: '280px' })
|
||
.setLngLat(e.lngLat)
|
||
.setHTML(`<strong><a href="/interventions/${props.id}/" target="_blank">${props.code || ''}</a></strong><br><span class="text-muted small">${props.title || ''}</span>`)
|
||
.addTo(calMap);
|
||
});
|
||
calMap.on('mouseenter', 'cal-ivt-circles', () => { calMap.getCanvas().style.cursor = 'pointer'; });
|
||
calMap.on('mouseleave', 'cal-ivt-circles', () => { calMap.getCanvas().style.cursor = ''; });
|
||
}
|
||
// Fit map to data features within the configured region extent
|
||
const extentBounds = (window.MAP_DEFAULT_EXTENT && window.MAP_DEFAULT_EXTENT.length === 2) ? {
|
||
minLng: window.MAP_DEFAULT_EXTENT[0][0],
|
||
minLat: window.MAP_DEFAULT_EXTENT[0][1],
|
||
maxLng: window.MAP_DEFAULT_EXTENT[1][0],
|
||
maxLat: window.MAP_DEFAULT_EXTENT[1][1]
|
||
} : {
|
||
minLng: 4.24,
|
||
maxLng: 4.48,
|
||
minLat: 50.76,
|
||
maxLat: 50.92
|
||
};
|
||
const pts = (fc.features || []).filter(f => {
|
||
const coords = f.geometry?.coordinates;
|
||
if (!coords || coords.length !== 2) return false;
|
||
const lng = coords[0];
|
||
const lat = coords[1];
|
||
return (lng >= extentBounds.minLng && lng <= extentBounds.maxLng &&
|
||
lat >= extentBounds.minLat && lat <= extentBounds.maxLat);
|
||
});
|
||
if (pts.length) {
|
||
const bounds = pts.reduce(
|
||
(b, f) => b.extend(f.geometry.coordinates),
|
||
new maplibregl.LngLatBounds(pts[0].geometry.coordinates, pts[0].geometry.coordinates)
|
||
);
|
||
calMap.fitBounds(bounds, { padding: 60, maxZoom: 16, duration: 500 });
|
||
} else {
|
||
calMap.easeTo({ center: [4.3572, 50.8477], zoom: 11, duration: 500 });
|
||
}
|
||
applyTeamMode();
|
||
})
|
||
.catch(err => console.error('Calendar map data error:', err));
|
||
}
|
||
|
||
function initMap() {
|
||
MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas';
|
||
MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl';
|
||
MapboxDraw.constants.classes.CONTROL_PREFIX= 'maplibregl-ctrl-';
|
||
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';
|
||
MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib';
|
||
|
||
calMap = new maplibregl.Map({
|
||
container: 'calendar-map',
|
||
style: MAPTILER_STYLE,
|
||
center: [4.3572, 50.8477],
|
||
zoom: 11
|
||
});
|
||
calMap.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||
|
||
calDraw = new MapboxDraw({
|
||
displayControlsDefault: false,
|
||
controls: { polygon: true, trash: true },
|
||
styles: DRAW_STYLES
|
||
});
|
||
|
||
calMap.on('load', () => {
|
||
calMap.addControl(calDraw);
|
||
mapReady = true;
|
||
loadMapData();
|
||
updateModeUI(); // hide draw buttons initially (viewport mode)
|
||
});
|
||
|
||
// Keep only one polygon at a time
|
||
calMap.on('draw.create', () => {
|
||
const all = calDraw.getAll().features;
|
||
if (all.length > 1) {
|
||
const last = all[all.length - 1].id;
|
||
calDraw.delete(all.filter(f => f.id !== last).map(f => f.id));
|
||
}
|
||
});
|
||
}
|
||
|
||
// Show modal → init or resize
|
||
modalEl?.addEventListener('shown.bs.modal', () => {
|
||
if (!calMap) {
|
||
initMap();
|
||
} else {
|
||
calMap.resize();
|
||
loadMapData();
|
||
}
|
||
});
|
||
|
||
// Radio mode change
|
||
document.querySelectorAll('input[name="geo-filter-mode"]').forEach(r => r.addEventListener('change', updateModeUI));
|
||
|
||
// Apply button
|
||
document.getElementById('geo-filter-apply')?.addEventListener('click', () => {
|
||
const mode = getMode();
|
||
bboxInput.value = '';
|
||
polyInput.value = '';
|
||
|
||
if (mode === 'viewport') {
|
||
if (!calMap) return;
|
||
const b = calMap.getBounds();
|
||
bboxInput.value = [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()].join(',');
|
||
} else {
|
||
const features = calDraw?.getAll().features || [];
|
||
if (!features.length) {
|
||
alert(gettext('Veuillez dessiner un polygone sur la carte.'));
|
||
return;
|
||
}
|
||
polyInput.value = JSON.stringify(features[0].geometry);
|
||
}
|
||
|
||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||
modal?.hide();
|
||
modalEl.addEventListener('hidden.bs.modal', function once() {
|
||
modalEl.removeEventListener('hidden.bs.modal', once);
|
||
filtersForm.requestSubmit ? filtersForm.requestSubmit() : filtersForm.submit();
|
||
});
|
||
});
|
||
|
||
// Team distribution toggle
|
||
document.getElementById('geo-team-toggle')?.addEventListener('click', function () {
|
||
teamMode = !teamMode;
|
||
this.classList.toggle('btn-outline-secondary', !teamMode);
|
||
this.classList.toggle('btn-secondary', teamMode);
|
||
applyTeamMode();
|
||
});
|
||
|
||
// Clear geo filter button
|
||
document.getElementById('geo-filter-clear')?.addEventListener('click', () => {
|
||
bboxInput.value = '';
|
||
polyInput.value = '';
|
||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||
modal?.hide();
|
||
modalEl.addEventListener('hidden.bs.modal', function once() {
|
||
modalEl.removeEventListener('hidden.bs.modal', once);
|
||
filtersForm.requestSubmit ? filtersForm.requestSubmit() : filtersForm.submit();
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock script %} |