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" %}
|
{% 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 %}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue