loko/streetup/interventions/templates/interventions/interventions_calendar.html

1027 lines
No EOL
55 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 %}