438 lines
22 KiB
HTML
438 lines
22 KiB
HTML
{% extends "base.html" %}
|
||
{% load static %}
|
||
{% load i18n %}
|
||
|
||
{% block head %}
|
||
<link rel="stylesheet" href="{% static 'observations/observations_index.css' %}">
|
||
<style>
|
||
/* Supprime le contour bleu Bootstrap au focus sur l'input de recherche */
|
||
#observations-search-input:focus {
|
||
outline: none;
|
||
box-shadow: none;
|
||
border-color: #dee2e6;
|
||
}
|
||
|
||
/* Styles pour la modale de filtre */
|
||
#filterModal .modal-dialog {
|
||
max-width: 95%;
|
||
}
|
||
#filterModal .modal-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: 90vh;
|
||
}
|
||
#filterModal .modal-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
#filterModal .modal-footer {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.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 */
|
||
.filter-option label.context-badge {
|
||
border-color: currentColor;
|
||
color: inherit;
|
||
background-color: transparent;
|
||
}
|
||
|
||
.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 */
|
||
.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; }
|
||
|
||
.filter-option.subcategory-option label {
|
||
font-size: 0.875em;
|
||
color: #555;
|
||
}
|
||
.filter-option-break {
|
||
flex-basis: 100%;
|
||
height: 0;
|
||
}
|
||
.filter-subcat-controls {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.filter-option input[type="checkbox"] {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Symptômes : boutons deux fois plus larges */
|
||
.filter-options[data-field="symptom"] .filter-option label {
|
||
width: 280px;
|
||
}
|
||
</style>
|
||
{% endblock head %}
|
||
|
||
{% block content %}
|
||
|
||
<!-- Modale de filtre des observations -->
|
||
<div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="filterModalLabel">{% translate "Filtrer les observations" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form method="get" id="filter-form" class="mb-4">
|
||
|
||
<!-- Thématique -->
|
||
<div class="col-12 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Thématique" %}</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-1"></i>{% endif %}{{ 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="category">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="category">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="category">
|
||
{% for id, name, thematic_code, parent_id in category_choices %}
|
||
{% if parent_id %}{% ifchanged parent_id %}<div class="filter-option-break"></div>{% endifchanged %}{% endif %}
|
||
<div class="filter-option{% if parent_id %} subcategory-option{% endif %}" data-thematic="{{ thematic_code }}" data-category-id="{{ id }}" data-parent-id="{{ parent_id|default:'' }}">
|
||
<input type="checkbox" name="category" id="category_{{ id }}" value="{{ id }}" {% if id|stringformat:"s" in selected_categories %}checked{% endif %}>
|
||
<label for="category_{{ id }}">{% if parent_id %}<i class="bi bi-arrow-return-right me-1 text-muted small"></i>{% endif %}{{ name }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Symptôme -->
|
||
<div class="col-12 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Symptôme" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="symptom">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="symptom">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="symptom">
|
||
{% for id, name, thematic_code, cat_ids in symptom_choices %}
|
||
<div class="filter-option" data-thematic="{{ thematic_code }}" data-categories="{{ cat_ids }}">
|
||
<input type="checkbox" name="symptom" id="symptom_{{ id }}" value="{{ id }}" {% if id|stringformat:"s" in selected_symptoms %}checked{% endif %}>
|
||
<label for="symptom_{{ id }}">{{ name }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Type d'observation -->
|
||
<div class="col-12 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Type d'observation" %}</label>
|
||
<div class="filter-section-buttons">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="type">{% translate "Tous" %}</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="type">{% translate "Aucun" %}</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-options" data-field="type">
|
||
{% for key, label, badge in observation_type_choices %}
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="type" id="obs-type_{{ key }}" value="{{ key }}" {% if key in selected_types %}checked{% endif %}>
|
||
<label for="obs-type_{{ key }}" class="context-badge btn-outline-{{ badge }}">{{ label }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Statut -->
|
||
<div class="col-12 filter-section">
|
||
<div class="filter-section-header">
|
||
<label>{% translate "Statut" %}</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, label, badge in status_choices %}
|
||
<div class="filter-option">
|
||
<input type="checkbox" name="status" id="obs-status_{{ key }}" value="{{ key }}" {% if key in selected_statuses %}checked{% endif %}>
|
||
<label for="obs-status_{{ key }}" class="context-badge btn-outline-{{ badge }}">{{ label }}</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Période -->
|
||
<div class="row mt-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label">{% translate "Période" %}</label>
|
||
<div class="d-flex gap-2 align-items-center">
|
||
<input type="date" name="date_from" id="date_from" class="form-control" value="{{ date_from }}" title="{% translate 'Du' %}">
|
||
<span class="text-muted small">–</span>
|
||
<input type="date" name="date_to" id="date_to" class="form-control" value="{{ date_to }}" title="{% translate 'Au' %}">
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="form-check mt-4">
|
||
<input class="form-check-input" type="checkbox" name="mine" value="1" id="mine" {% if show_mine_only %}checked{% endif %}>
|
||
<label class="form-check-label" for="mine">{% translate "Mes observations" %}</label>
|
||
</div>
|
||
{% if user_has_team %}
|
||
<div class="form-check mt-2">
|
||
<input class="form-check-input" type="checkbox" name="team" value="1" id="my-team" {% if show_my_team %}checked{% endif %}>
|
||
<label class="form-check-label" for="my-team">{% translate "À traiter par mon équipe" %}</label>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" id="reset-filters" class="btn btn-secondary">{% translate "Réinitialiser" %}</button>
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||
<button type="button" id="apply-filters" class="btn btn-primary">{% translate "Filtrer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="position-relative">
|
||
<!-- Layout selection buttons -->
|
||
<div class="position-absolute top-0 end-0 mt-2" style="z-index: 10;">
|
||
<div class="btn-group" role="group" aria-label="{% translate 'Sélection de la vue' %}">
|
||
<button id="layout-btn-table" class="btn btn-light" type="button" title="{% translate 'Vue tableau uniquement' %}">
|
||
<i class="bi bi-table"></i>
|
||
</button>
|
||
<button id="layout-btn-both" class="btn btn-light text-primary" type="button" title="{% translate 'Vue partagée' %}">
|
||
<i class="bi bi-layout-split"></i>
|
||
</button>
|
||
<button id="layout-btn-map" class="btn btn-light" type="button" title="{% translate 'Vue carte uniquement' %}">
|
||
<i class="bi bi-globe-europe-africa"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Widget de recherche — centré sur toute la largeur (tableau + carte) -->
|
||
<div id="observations-search-widget"
|
||
style="position:absolute; top:4px; left:50%; transform:translateX(-50%); z-index:10; pointer-events:none;">
|
||
<div class="input-group" style="pointer-events:auto; flex-wrap:nowrap;">
|
||
<div style="position:relative; display:flex;">
|
||
<input type="text" id="observations-search-input" class="form-control form-control-sm"
|
||
style="width:320px; border-radius:20px 0 0 20px; border-color:#dee2e6; padding-right:1.6rem;"
|
||
placeholder="{% translate 'Description ou adresse…' %}">
|
||
<button type="button" id="observations-search-clear"
|
||
style="position:absolute; right:4px; top:50%; transform:translateY(-50%);
|
||
background:none; border:none; padding:0 2px; line-height:1;
|
||
color:#adb5bd; cursor:pointer; visibility:hidden;"
|
||
title="{% translate 'Effacer la recherche' %}">
|
||
<i class="bi bi-x-lg" style="font-size:0.7rem;"></i>
|
||
</button>
|
||
</div>
|
||
<span class="input-group-text bg-white px-1" style="border-left:0; border-right:0; border-color:#dee2e6;">
|
||
<div class="btn-group btn-group-sm" role="group" aria-label="{% translate 'Mode' %}">
|
||
<input type="radio" class="btn-check" name="obs-search-mode" id="obs-search-mode-and" value="and" checked>
|
||
<label class="btn btn-outline-secondary py-0 px-2" for="obs-search-mode-and" title="{% translate 'Tous les mots doivent correspondre' %}">{% translate "AND" %}</label>
|
||
<input type="radio" class="btn-check" name="obs-search-mode" id="obs-search-mode-or" value="or">
|
||
<label class="btn btn-outline-secondary py-0 px-2" for="obs-search-mode-or" title="{% translate 'Au moins un mot doit correspondre' %}">{% translate "OR" %}</label>
|
||
</div>
|
||
</span>
|
||
<button class="btn btn-sm" type="button" id="observations-search-btn"
|
||
style="border-radius:0 20px 20px 0; border:1px solid #dee2e6; border-left:0; background:white; color:#495057;"
|
||
title="{% translate 'Rechercher' %}">
|
||
<i class="bi bi-search"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row mt-2 observations-layout" style="height: calc(100vh - 130px);">
|
||
<div class="col-md-6" id="col-left" style="max-height: calc(100vh - 130px);">
|
||
<ul class="nav nav-tabs" id="tablesTab" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" id="observations-table-tab" data-bs-toggle="tab" data-bs-target="#observations-table-content" type="button" role="tab" aria-controls="observations-table" aria-selected="true">{% translate "Observations" %}</button>
|
||
</li>
|
||
|
||
<div class="my-1 ms-2">
|
||
<a href="{% url 'observations:observation_add' %}" class="btn btn-light">
|
||
<i class="bi bi-plus-circle"></i>
|
||
</a>
|
||
</div>
|
||
</ul>
|
||
|
||
<div class="tab-content my-2" id="tablesTabContent">
|
||
<div class="tab-pane fade show active" id="observations-table-content" role="tabpanel" aria-labelledby="observations-table-tab">
|
||
<div class="d-flex justify-content-between flex-wrap align-items-center gap-2 mt-3">
|
||
<div class="text-muted small" id="observations-result-count">
|
||
{% translate "Aucune observation chargée" %}
|
||
</div>
|
||
<span id="filter-limit-alert" class="alert alert-info py-1 px-2 mb-0 d-none">
|
||
<i class="bi bi-info-circle me-1"></i>
|
||
{% translate "Contenu limité à 5 000 éléments." %}
|
||
</span>
|
||
</div>
|
||
|
||
<div id="observations-spinner" class="table-spinner">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">{% translate "Chargement..." %}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="observations-table-container" class="d-none">
|
||
<div id="filter-dropdown-wrapper" class="d-flex align-items-center gap-2">
|
||
<button type="button" class="btn btn-light" data-bs-toggle="modal" data-bs-target="#filterModal">
|
||
{% translate "Filtre" %}
|
||
</button>
|
||
<a id="export-excel-btn" href="{% url 'observations:observations_export_excel' %}" class="btn btn-light" title="{% translate 'Exporter en Excel' %}">
|
||
<i class="bi bi-file-earmark-spreadsheet"></i>
|
||
</a>
|
||
</div>
|
||
|
||
<table id="observations-table" class="table"
|
||
data-height="550"
|
||
data-pagination="true"
|
||
data-page-size="50"
|
||
data-show-columns="true"
|
||
data-unique-id="id"
|
||
data-sort-name="created_at"
|
||
data-sort-order="desc">
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-6" id="col-right">
|
||
<div id="map" class="rounded-2 observations-map">
|
||
<button id="filterzone" class="btn btn-light btn-sm observations-map-filter" type="button">
|
||
{% translate "Filtrer sur cette zone" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock content %}
|
||
|
||
{% block script %}
|
||
<script>
|
||
const observationsgeojsonUrl = "{% url 'observations:observations_geojson' %}";
|
||
const observationsIndexUrl = "{% url 'observations:observations_index' %}";
|
||
const observationsExportExcelUrl = "{% url 'observations:observations_export_excel' %}";
|
||
const savePreferencesUrl = "{% url 'common:save_observation_list_preferences' %}";
|
||
const SAVED_SORT = {{ saved_sort|default:"{}"|safe }};
|
||
const subcatNoneLabel = "{% translate '(aucune sous-catégorie)' %}";
|
||
</script>
|
||
<script src="{% url 'javascript-catalog' %}"></script>
|
||
<script src="{% static 'common/base_script.js' %}"></script>
|
||
<script src="{% static 'common/base_map.js' %}"></script>
|
||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
||
<script src="{% static 'observations/observations_index.js' %}"></script>
|
||
{% endblock script %}
|