feat: add Localisation, Documents and Interventions tabs to public building detail view

This commit is contained in:
kdeterme 2026-07-27 08:29:13 +02:00
parent ca5d17261b
commit 5db00ec09c
2 changed files with 262 additions and 9 deletions

View file

@ -1,8 +1,14 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static i18n %} {% load static i18n documents_tags %}
{% block title %}{{ building.code }} - {{ building.get_name }} | StreetUp{% endblock %} {% block title %}{{ building.code }} - {{ building.get_name }} | StreetUp{% endblock %}
{% block extra_head %}
<!-- MapLibre GL CSS -->
<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<!-- Breadcrumb & Actions --> <!-- Breadcrumb & Actions -->
@ -61,19 +67,41 @@
<!-- Tabs Navigation --> <!-- Tabs Navigation -->
<ul class="nav nav-tabs mb-4" id="buildingTabs" role="tablist"> <ul class="nav nav-tabs mb-4" id="buildingTabs" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab"><i class="bi bi-info-circle me-1"></i>{% translate "Vue d'ensemble" %}</button> <button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab">
<i class="bi bi-info-circle me-1"></i>{% translate "Vue d'ensemble" %}
</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="levels-tab" data-bs-toggle="tab" data-bs-target="#levels" type="button" role="tab"><i class="bi bg-layers me-1"></i>{% translate "Étages & Pièces" %} ({{ levels.count }})</button> <button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location" type="button" role="tab">
<i class="bi bi-geo-alt me-1"></i>{% translate "Localisation & Carte" %}
</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="equipments-tab" data-bs-toggle="tab" data-bs-target="#equipments" type="button" role="tab"><i class="bi bi-gear me-1"></i>{% translate "Équipements techniques" %} ({{ equipments.count }})</button> <button class="nav-link" id="levels-tab" data-bs-toggle="tab" data-bs-target="#levels" type="button" role="tab">
<i class="bi bi-layers me-1"></i>{% translate "Étages & Pièces" %} ({{ levels.count }})
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="equipments-tab" data-bs-toggle="tab" data-bs-target="#equipments" type="button" role="tab">
<i class="bi bi-gear me-1"></i>{% translate "Équipements" %} ({{ equipments.count }})
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="documents-tab" data-bs-toggle="tab" data-bs-target="#documents" type="button" role="tab">
<i class="bi bi-file-earmark-text me-1"></i>{% translate "Documents" %} ({{ documents.count }})
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="interventions-tab" data-bs-toggle="tab" data-bs-target="#interventions" type="button" role="tab">
<i class="bi bi-tools me-1"></i>{% translate "Interventions" %} ({{ interventions.count }})
</button>
</li> </li>
</ul> </ul>
<!-- Tabs Content --> <!-- Tabs Content -->
<div class="tab-content" id="buildingTabsContent"> <div class="tab-content" id="buildingTabsContent">
<!-- Overview Tab -->
<!-- 1. Overview Tab -->
<div class="tab-pane fade show active" id="overview" role="tabpanel"> <div class="tab-pane fade show active" id="overview" role="tabpanel">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-6"> <div class="col-md-6">
@ -130,7 +158,50 @@
</div> </div>
</div> </div>
<!-- Levels & Rooms Tab --> <!-- 2. Localisation Tab -->
<div class="tab-pane fade" id="location" role="tabpanel">
<div class="row g-4">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
<span><i class="bi bi-map me-2"></i>{% translate "Emprise cartographique" %}</span>
{% if center_lat and center_lon %}
<a href="https://www.google.com/maps?cbll={{ center_lat|floatformat:'6u'}},{{ center_lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid="
target="_blank" class="btn btn-sm btn-outline-secondary">
<img src="{% static 'common/google-street-view-logo.png' %}" alt="Street View" height="16" class="me-1">
{% translate "Google Street View" %}
</a>
{% endif %}
</div>
<div class="card-body p-0">
<div id="building-map" style="height: 440px; width: 100%; border-radius: 0 0 4px 4px;"></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-light fw-bold"><i class="bi bi-geo-alt me-2"></i>{% translate "Adresse & Coordonnées" %}</div>
<div class="card-body">
<p class="mb-3">
<strong class="d-block text-muted mb-1">{% translate "Adresse complète" %} :</strong>
{{ building.address|default:_("Non renseignée") }}
</p>
{% if center_lat and center_lon %}
<div class="p-3 bg-light rounded border mb-3">
<div class="small text-muted mb-1">{% translate "Coordonnées WGS84 (Lat, Lon)" %} :</div>
<div class="font-monospace fw-bold">{{ center_lat|floatformat:6 }}, {{ center_lon|floatformat:6 }}</div>
</div>
{% endif %}
<div class="alert alert-info py-2 px-3 mb-0 small">
<i class="bi bi-info-circle me-1"></i>{% translate "Emprise vectorielle (MultiPolygone SRID 3812)." %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 3. Levels & Rooms Tab -->
<div class="tab-pane fade" id="levels" role="tabpanel"> <div class="tab-pane fade" id="levels" role="tabpanel">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
@ -157,7 +228,7 @@
</div> </div>
</div> </div>
<!-- Equipments Tab --> <!-- 4. Equipments Tab -->
<div class="tab-pane fade" id="equipments" role="tabpanel"> <div class="tab-pane fade" id="equipments" role="tabpanel">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body p-0"> <div class="card-body p-0">
@ -188,6 +259,159 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 5. Documents Tab -->
<div class="tab-pane fade" id="documents" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title h6 mb-0"><i class="bi bi-file-earmark-text me-2"></i>{% translate "Documents associés" %}</h5>
<a href="{% url 'admin:assets_publicbuildingdocument_add' %}?building={{ building.pk }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-plus-lg me-1"></i>{% translate "Ajouter un document" %}
</a>
</div>
<div class="card-body">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-3">
{% for doc in documents %}
<div class="col">
<div class="card h-100 border shadow-sm text-center">
<div class="card-body d-flex flex-column align-items-center justify-content-center p-3">
{% if doc.document.file %}
<i class="bi bi-file-earmark-pdf text-danger fs-1 mb-2"></i>
<h6 class="card-title text-truncate w-100 mb-1" title="{{ doc.document.file.name }}">{{ doc.document.file.name }}</h6>
<a href="{{ doc.document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary mt-2">
<i class="bi bi-download me-1"></i>{% translate "Télécharger" %}
</a>
{% else %}
<i class="bi bi-file-earmark text-secondary fs-1 mb-2"></i>
<span class="text-muted small">{% translate "Fichier indisponible" %}</span>
{% endif %}
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center text-muted py-4">
<i class="bi bi-folder-x fs-2 d-block mb-2"></i>
{% translate "Aucun document associé à ce bâtiment public." %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- 6. Interventions Tab -->
<div class="tab-pane fade" id="interventions" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title h6 mb-0"><i class="bi bi-tools me-2"></i>{% translate "Interventions & Maintenance" %}</h5>
<a href="{% url 'interventions:interventions_index' %}?asset={{ building.pk }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-plus-lg me-1"></i>{% translate "Nouvelle intervention" %}
</a>
</div>
<div class="card-body p-0">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width: 50px;"></th>
<th>{% translate "Code" %}</th>
<th>{% translate "Titre / Objet" %}</th>
<th>{% translate "Date de début" %}</th>
<th>{% translate "Statut" %}</th>
</tr>
</thead>
<tbody>
{% for interv in interventions %}
<tr>
<td>
<a href="{% url 'interventions:interventions_index' %}{{ interv.id }}" class="btn btn-sm btn-light border" title="{% translate 'Détail' %}">
<i class="bi bi-info-circle"></i>
</a>
</td>
<td><strong>{{ interv.code }}</strong></td>
<td>{{ interv.title }}</td>
<td>{{ interv.begin_time|date:"d/m/Y H:i"|default:"-" }}</td>
<td><span class="badge bg-secondary">{{ interv.get_status_display }}</span></td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center text-muted py-4">
<i class="bi bi-tools fs-2 d-block mb-2 text-muted"></i>
{% translate "Aucune intervention enregistrée pour ce bâtiment." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block script %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const buildingGeom = {{ building_geojson_json|safe }};
const centerLon = {{ center_lon|default:4.3572 }};
const centerLat = {{ center_lat|default:50.8477 }};
let mapInitialized = false;
let map = null;
function initMap() {
if (mapInitialized) return;
const container = document.getElementById('building-map');
if (!container) return;
map = new maplibregl.Map({
container: 'building-map',
style: 'https://api.maptiler.com/maps/streets-v2-light/style.json?key=get_your_own_OpJZ2523Ko6W549ab5Bw',
center: [centerLon, centerLat],
zoom: 16
});
map.addControl(new maplibregl.NavigationControl());
map.on('load', function() {
if (buildingGeom) {
map.addSource('building-footprint', {
'type': 'geojson',
'data': buildingGeom
});
map.addLayer({
'id': 'building-fill',
'type': 'fill',
'source': 'building-footprint',
'paint': {
'fill-color': '#2B579A',
'fill-opacity': 0.4
}
});
map.addLayer({
'id': 'building-outline',
'type': 'line',
'source': 'building-footprint',
'paint': {
'line-color': '#2B579A',
'line-width': 3
}
});
}
});
mapInitialized = true;
}
const locationTab = document.getElementById('location-tab');
if (locationTab) {
locationTab.addEventListener('shown.bs.tab', function () {
initMap();
if (map) map.resize();
});
}
});
</script>
{% endblock script %}

View file

@ -182,13 +182,37 @@ def buildings_index(request):
@login_required @login_required
def building_detail(request, pk): def building_detail(request, pk):
""" """
Vue détaillée d'un Bâtiment Public avec ses niveaux, pièces et équipements. Vue détaillée d'un Bâtiment Public avec ses niveaux, pièces, équipements, documents, interventions et cartographie.
""" """
from django.contrib.contenttypes.models import ContentType
from interventions.models import Intervention
from interventions.permissions import filter_viewable_interventions_for_user
building = get_object_or_404(PublicBuilding, pk=pk) building = get_object_or_404(PublicBuilding, pk=pk)
levels = building.levels.all().prefetch_related('rooms') levels = building.levels.all().prefetch_related('rooms')
rooms = building.rooms.all() rooms = building.rooms.all()
equipments = building.equipments.all() equipments = building.equipments.all()
documents = building.documents.select_related('document').all() documents = building.documents.all()
content_type = ContentType.objects.get_for_model(PublicBuilding)
# Récupération des interventions associées au bâtiment
interventions_qs = Intervention.objects.filter(
interventionasset__object_id=building.id,
interventionasset__content_type=content_type
).distinct().order_by('-creation_time')
interventions = filter_viewable_interventions_for_user(request.user, interventions_qs)
# GeoJSON pour la carte de localisation
building_geojson = None
center_lon, center_lat = None, None
if building.geom:
geom_4326 = building.geom.clone()
geom_4326.transform(4326)
building_geojson = json.loads(geom_4326.json)
centroid = geom_4326.centroid
center_lon, center_lat = centroid.x, centroid.y
context = { context = {
'building': building, 'building': building,
@ -196,5 +220,10 @@ def building_detail(request, pk):
'rooms': rooms, 'rooms': rooms,
'equipments': equipments, 'equipments': equipments,
'documents': documents, 'documents': documents,
'interventions': interventions,
'content_type': content_type,
'building_geojson_json': json.dumps(building_geojson) if building_geojson else 'null',
'center_lon': center_lon,
'center_lat': center_lat,
} }
return render(request, 'assets/buildings/publicbuilding_detail.html', context) return render(request, 'assets/buildings/publicbuilding_detail.html', context)