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

438 lines
22 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 '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 %}