feat: add Localisation, Documents and Interventions tabs to public building detail view
This commit is contained in:
parent
ca5d17261b
commit
5db00ec09c
2 changed files with 262 additions and 9 deletions
|
|
@ -1,8 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
{% load static i18n documents_tags %}
|
||||
|
||||
{% 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 %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Breadcrumb & Actions -->
|
||||
|
|
@ -61,19 +67,41 @@
|
|||
<!-- Tabs Navigation -->
|
||||
<ul class="nav nav-tabs mb-4" id="buildingTabs" role="tablist">
|
||||
<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 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 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>
|
||||
</ul>
|
||||
|
||||
<!-- Tabs Content -->
|
||||
<div class="tab-content" id="buildingTabsContent">
|
||||
<!-- Overview Tab -->
|
||||
|
||||
<!-- 1. Overview Tab -->
|
||||
<div class="tab-pane fade show active" id="overview" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
|
|
@ -130,7 +158,50 @@
|
|||
</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="card shadow-sm">
|
||||
<div class="card-body">
|
||||
|
|
@ -157,7 +228,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equipments Tab -->
|
||||
<!-- 4. Equipments Tab -->
|
||||
<div class="tab-pane fade" id="equipments" role="tabpanel">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
|
|
@ -188,6 +259,159 @@
|
|||
</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>
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -182,13 +182,37 @@ def buildings_index(request):
|
|||
@login_required
|
||||
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)
|
||||
levels = building.levels.all().prefetch_related('rooms')
|
||||
rooms = building.rooms.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 = {
|
||||
'building': building,
|
||||
|
|
@ -196,5 +220,10 @@ def building_detail(request, pk):
|
|||
'rooms': rooms,
|
||||
'equipments': equipments,
|
||||
'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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue