loko/loko/assets/templates/assets/buildings/publicbuilding_detail.html
kdeterme 63c4a8e539 git add loko/assets/models/buildings.py \
loko/assets/forms.py \
        loko/assets/admin.py \
        loko/assets/templates/assets/buildings/ \
        loko/assets/tests.py \
        loko/assets/migrations/0028_buildingasset_contractor_name_and_more.py

git commit -m "feat(assets): extend building locations and technical equipment models

- Add comprehensive building locations (stairwells, technical rooms, boiler rooms, offices, etc.)
- Add building physical & technical fields (room numbers, floor types, heated status, dimensions)
- Add complete building envelope & technical equipment types (roofs, facades, windows, doors, boilers, heat pumps, etc.)
- Enrich admin, forms, trees, and detail templates with icons, macro-lots, and maintenance tracking
- Add migration 0028 and unit test coverage"
2026-10-05 09:49:08 +02:00

570 lines
34 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 i18n documents_tags %}
{% block title %}{{ building.code }} - {{ building.get_name }} | {{ product_name }}{% 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 -->
<div class="d-flex justify-content-between align-items-center mb-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'assets:buildings_index' %}">{% translate "Bâtiments publics" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ building.code }}</li>
</ol>
</nav>
<div class="d-flex gap-2">
{% if can_edit %}
<a href="{% url 'assets:publicbuilding_edit' building.pk %}" class="btn btn-primary btn-sm">
<i class="bi bi-pencil me-1"></i>{% translate "Modifier" %}
</a>
{% endif %}
<a href="{% url 'assets:buildings_index' %}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>{% translate "Retour à la liste" %}
</a>
</div>
</div>
<!-- Header Card -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h2 class="h4 mb-1">
<i class="bi bi-building text-primary me-2"></i>{{ building.code }} - {{ building.get_name }}
</h2>
<p class="text-muted mb-2"><i class="bi bi-geo-alt me-1"></i>{{ building.address|default:_("Adresse non renseignée") }}</p>
<div class="d-flex gap-2 flex-wrap">
<span class="badge bg-primary-subtle text-primary border">{{ building.get_usage_type_display }}</span>
<span class="badge bg-success-subtle text-success border">{{ building.get_building_status_display }}</span>
{% if building.is_erp %}
<span class="badge bg-info-subtle text-info border">{{ regional_terms.public_access_short }} (Cat. {{ building.erp_category|default:"-" }})</span>
{% endif %}
</div>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
{% if building.dpe_rating %}
<div class="d-inline-block text-center me-3">
<span class="d-block text-muted small">{{ regional_terms.energy_diag_short }}</span>
<span class="badge bg-success fs-5 px-3 py-2">Classe {{ building.dpe_rating }}</span>
</div>
{% endif %}
{% if building.ges_rating %}
<div class="d-inline-block text-center">
<span class="d-block text-muted small">{{ regional_terms.carbon_diag_short }}</span>
<span class="badge bg-primary fs-5 px-3 py-2">Classe {{ building.ges_rating }}</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 3 Core 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>
</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 "Assets & Équipements" %} ({{ equipments|length }})
</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">
<!-- 1. Vue d'ensemble (Dashboard avec Cards directes) -->
<div class="tab-pane fade show active" id="overview" role="tabpanel">
<div class="row g-4">
<!-- Left Column: Propriété, Caractéristiques & Arborescence Niveaux/Locaux/Équipements -->
<div class="col-md-7">
<!-- Card 1: Propriété & Responsables -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold"><i class="bi bi-person-badge me-2"></i>{% translate "Propriété & Responsables" %}</div>
<div class="card-body">
<table class="table table-borderless mb-0">
<tr>
<th class="w-50 text-muted">{% translate "Propriétaire" %} :</th>
<td>{{ building.owner|default:"-" }}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Gestionnaire référent" %} :</th>
<td>{% if building.manager %}{{ building.manager.get_full_name|default:building.manager.username }}{% else %}-{% endif %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Service occupant" %} :</th>
<td>{{ building.occupant_entity|default:"-" }}</td>
</tr>
</table>
</div>
</div>
<!-- Card 2: Caractéristiques physiques & Normes -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold"><i class="bi bi-rulers me-2"></i>{% translate "Caractéristiques physiques &" %} {{ regional_terms.public_access_short }}</div>
<div class="card-body">
<table class="table table-borderless mb-0">
<tr>
<th class="w-50 text-muted">{% translate "Année de construction" %} :</th>
<td>{{ building.construction_year|default:"-" }}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Étages / Sous-sols" %} :</th>
<td>{{ building.floors_count }} {% translate "étage(s)" %} / {{ building.basement_count }} {% translate "sous-sol(s)" %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Surface Utile / Brut" %} :</th>
<td>{{ building.usable_surface_m2|default:"-" }} m² / {{ building.total_surface_m2|default:"-" }} m²</td>
</tr>
{% if building.room_number %}
<tr>
<th class="text-muted">{% translate "Numéro de local / porte" %} :</th>
<td><span class="badge bg-secondary-subtle text-secondary border font-monospace"><i class="bi bi-tag me-1"></i>{{ building.room_number }}</span></td>
</tr>
{% endif %}
{% if building.surface_m2 %}
<tr>
<th class="text-muted">{% translate "Surface spécifique" %} :</th>
<td>{{ building.surface_m2 }} m²</td>
</tr>
{% endif %}
{% if building.ceiling_height_m %}
<tr>
<th class="text-muted">{% translate "Hauteur sous plafond" %} :</th>
<td>{{ building.ceiling_height_m }} m</td>
</tr>
{% endif %}
<tr>
<th class="text-muted">{% translate "Chauffage du local" %} :</th>
<td>{% if building.is_heated %}<span class="text-success"><i class="bi bi-fire me-1"></i>{% translate "Chauffé" %}</span>{% else %}<span class="text-muted"><i class="bi bi-slash-circle me-1"></i>{% translate "Non chauffé" %}</span>{% endif %}</td>
</tr>
{% if building.floor_type %}
<tr>
<th class="text-muted">{% translate "Revêtement de sol" %} :</th>
<td>{{ building.get_floor_type_display }}</td>
</tr>
{% endif %}
{% if building.fire_compartment %}
<tr>
<th class="text-muted">{% translate "Compartiment coupe-feu" %} :</th>
<td>{{ building.fire_compartment }}</td>
</tr>
{% endif %}
<tr>
<th class="text-muted">{{ regional_terms.public_access_category }} :</th>
<td>{{ building.erp_category|default:"-" }}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Accessibilité PMR" %} :</th>
<td>{% if building.pmr_accessible %}<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>{% translate "Oui" %}</span>{% else %}<span class="text-muted">{% translate "Non" %}</span>{% endif %}</td>
</tr>
</table>
</div>
</div>
<!-- Card 3: Arborescence Unifiée des Localisations & Assets -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
<span><i class="bi bi-diagram-3 me-2"></i>{% translate "Arborescence Hiérarchique" %}</span>
<span class="badge bg-primary-subtle text-primary border">{{ location_tree.children|length }} {% translate "sous-localisation(s)" %}</span>
</div>
<div class="card-body p-3">
<!-- Localisation Parente (si existante) -->
{% if parent_location %}
<div class="card bg-light border-secondary-subtle p-2 mb-3">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-geo-alt-fill text-danger fs-5"></i>
<div>
<div class="small text-muted fw-semibold">{% translate "Localisation parente" %} :</div>
<a href="{{ parent_location.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline">
{{ parent_location.code }} - {{ parent_location.get_name }}
</a>
<span class="badge bg-secondary-subtle text-dark border small ms-1">{{ parent_location.get_location_type_display }}</span>
</div>
</div>
<a href="{{ parent_location.get_absolute_url }}" class="btn btn-sm btn-outline-secondary py-0 px-2 small">
<i class="bi bi-arrow-up me-1"></i>{% translate "Voir la parente" %}
</a>
</div>
</div>
{% endif %}
<!-- Localisation Courante (Racine de l'affichage) -->
<div class="p-2 border rounded bg-white mb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<i class="bi {{ building.get_icon_class }} text-primary fs-5"></i>
<span class="fw-bold fs-6 text-primary">{{ building.code }}</span>
{% if building.room_number %}
<span class="badge bg-secondary-subtle text-secondary border ms-1 font-monospace"><i class="bi bi-tag me-1"></i>{{ building.room_number }}</span>
{% endif %}
<span class="fw-bold fs-6">- {{ building.get_name }}</span>
<span class="badge bg-primary text-white ms-1">{{ building.get_location_type_display }}</span>
{% if location_tree.assets %}
<button class="btn btn-sm btn-outline-info py-0 px-2 small ms-2 text-nowrap"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-assets-root"
aria-expanded="false">
<i class="bi bi-tools me-1"></i>Assets ({{ location_tree.assets|length }})
</button>
{% endif %}
</div>
</div>
<!-- Collapsible Assets de la localisation courante (NON VISIBLES PAR DÉFAUT) -->
{% if location_tree.assets %}
<div id="tree-assets-root" class="collapse mt-2 ms-4">
<div class="card card-body bg-light border-info-subtle p-2 mb-2">
<div class="fw-bold small text-info mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ building.code }} :</div>
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=location_tree.assets only %}
</div>
</div>
{% endif %}
<!-- Sous-localisations & leurs assets -->
<div class="mt-2">
{% include "assets/buildings/publicbuilding_location_tree.html" with node=location_tree depth=1 only %}
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Localisation & Documents (sous la carte) -->
<div class="col-md-5">
<!-- Card Localisation & Carte interactive -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
<span><i class="bi bi-geo-alt me-2"></i>{% translate "Localisation & Carte" %}</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 py-0 px-2">
<img src="{% static 'common/google-street-view-logo.png' %}" alt="Street View" height="14" class="me-1">
Street View
</a>
{% endif %}
</div>
<div class="card-body p-0">
<div id="building-map" style="height: 340px; width: 100%; border-radius: 0 0 4px 4px;"></div>
</div>
<div class="card-footer bg-white p-3">
<p class="mb-2 small">
<strong>{% translate "Adresse" %} :</strong> {{ building.address|default:_("Non renseignée") }}
</p>
{% if center_lat and center_lon %}
<div class="small text-muted font-monospace">
WGS84: {{ center_lat|floatformat:6 }}, {{ center_lon|floatformat:6 }}
</div>
{% endif %}
</div>
</div>
<!-- Card Documents associés (placée sous la localisation) -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<span class="fw-bold"><i class="bi bi-file-earmark-text me-2"></i>{% translate "Documents associés" %} ({{ documents.count }})</span>
<a href="{% url 'admin:assets_publicbuildingdocument_add' %}?building={{ building.pk }}" class="btn btn-sm btn-outline-primary py-0 px-2">
<i class="bi bi-plus-lg me-1"></i>{% translate "Ajouter" %}
</a>
</div>
<div class="card-body">
<div class="row row-cols-1 row-cols-sm-2 g-3">
{% for doc in documents %}
<div class="col">
<div class="card h-100 border text-center p-2">
{% if doc.document.file %}
<i class="bi bi-file-earmark-pdf text-danger fs-2 mb-1"></i>
<div class="small text-truncate w-100 fw-bold" title="{{ doc.document.file.name }}">{{ doc.document.file.name }}</div>
<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-2 mb-1"></i>
<span class="text-muted small">{% translate "Fichier indisponible" %}</span>
{% endif %}
</div>
</div>
{% empty %}
<div class="col-12 text-center text-muted py-3">
<i class="bi bi-folder-x fs-4 d-block mb-1"></i>
{% translate "Aucun document associé." %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. Assets & Équipements Tab -->
<div class="tab-pane fade" id="equipments" 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 "Assets Physiques & Équipements de la Localisation et de ses sous-localisations" %}</h5>
<span class="badge bg-info-subtle text-info border">{{ equipments|length }} {% translate "asset(s)" %}</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{% translate "Code / Nom" %}</th>
<th>{% translate "Lot / Type d'asset" %}</th>
<th>{% translate "Caractéristiques & Spécifications" %}</th>
<th>{% translate "Localisation spécifique" %}</th>
<th>{% translate "Asset Parent" %}</th>
<th>{% translate "Statut" %}</th>
<th>{% translate "Prochain contrôle" %}</th>
<th>{% translate "Maintenance & Prestataire" %}</th>
<th class="text-end pe-3">{% translate "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for eq in equipments %}
<tr>
<td>
<a href="{{ eq.get_absolute_url }}" class="fw-bold text-decoration-none">
<i class="bi {{ eq.get_icon_class }} me-1 text-primary"></i>{{ eq.code }}
</a>
<div class="small text-muted">{{ eq.get_name }}</div>
</td>
<td>
<span class="badge bg-primary-subtle text-primary border me-1">{{ eq.get_macro_lot }}</span>
<span class="badge bg-info-subtle text-info border d-block mt-1">
{{ eq.get_asset_type_display }}
</span>
</td>
<td>
{% if eq.dimensions or eq.surface_m2 %}
<div class="small fw-semibold">{% if eq.dimensions %}{{ eq.dimensions }}{% endif %}{% if eq.surface_m2 %} ({{ eq.surface_m2 }} m²){% endif %}</div>
{% endif %}
{% if eq.power_kw or eq.energy_source %}
<div class="small text-muted">
{% if eq.power_kw %}<i class="bi bi-lightning-charge me-1"></i>{{ eq.power_kw }} kW{% endif %}
{% if eq.energy_source %}<span class="badge bg-warning-subtle text-dark border ms-1">{{ eq.get_energy_source_display }}</span>{% endif %}
</div>
{% endif %}
{% if eq.material or eq.fire_rating or eq.glazing_type %}
<div class="small text-muted">
{% if eq.material %}{{ eq.material }}{% endif %}
{% if eq.glazing_type %} - {{ eq.get_glazing_type_display }}{% endif %}
{% if eq.fire_rating %}<span class="badge bg-danger-subtle text-danger border ms-1">{{ eq.fire_rating }}</span>{% endif %}
</div>
{% endif %}
</td>
<td>
{% if eq.location %}
<a href="{{ eq.location.get_absolute_url }}" class="small text-dark text-decoration-none">
<i class="bi {{ eq.location.get_icon_class }} me-1"></i>{{ eq.location.code }} - {{ eq.location.get_name }}
</a>
{% else %}
<span class="small text-muted">{{ eq.building.code }}</span>
{% endif %}
</td>
<td>
{% if eq.parent_asset %}
<a href="{{ eq.parent_asset.get_absolute_url }}" class="small text-decoration-none">
<i class="bi {{ eq.parent_asset.get_icon_class }} me-1"></i>{{ eq.parent_asset.code }}
</a>
{% else %}
<span class="text-muted small">-</span>
{% endif %}
</td>
<td>
<span class="badge {% if eq.status == 'active' %}bg-success-subtle text-success border{% else %}bg-secondary-subtle text-secondary border{% endif %}">
{{ eq.get_status_display }}
</span>
</td>
<td>{{ eq.next_control_date|date:"d/m/Y"|default:"-" }}</td>
<td>
{% if eq.contractor_name %}
<div class="small fw-semibold"><i class="bi bi-building me-1"></i>{{ eq.contractor_name }}</div>
{% endif %}
{% if eq.maintenance_contract_ref %}
<div class="small text-muted font-monospace">Réf: {{ eq.maintenance_contract_ref }}</div>
{% elif not eq.contractor_name %}
<span class="text-muted small">-</span>
{% endif %}
</td>
<td class="text-end pe-3">
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap" title="{% translate 'Détail de l’asset' %}">
<i class="bi bi-eye me-1"></i>{% translate "Détail" %}
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center text-muted py-4">{% translate "Aucun asset ou équipement technique enregistré." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 3. 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 map = null;
function initMap() {
const container = document.getElementById('building-map');
if (!container) return;
map = new maplibregl.Map({
container: 'building-map',
style: '{{ MAP_STYLE_LIGHT|default:"https://api.maptiler.com/maps/streets-v2-light/style.json" }}',
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
}
});
}
});
}
initMap();
const overviewTab = document.getElementById('overview-tab');
if (overviewTab) {
overviewTab.addEventListener('shown.bs.tab', function () {
if (map) map.resize();
});
}
// Gestion du chevron plié / déplié dans l'arborescence
document.querySelectorAll('.tree-toggle-btn').forEach(function(btn) {
const targetId = btn.getAttribute('data-bs-target');
const target = document.querySelector(targetId);
if (target) {
target.addEventListener('show.bs.collapse', function (e) {
if (e.target !== target) return;
const icon = btn.querySelector('i');
if (icon) {
icon.classList.remove('bi-chevron-right');
icon.classList.add('bi-chevron-down');
}
});
target.addEventListener('hide.bs.collapse', function (e) {
if (e.target !== target) return;
const icon = btn.querySelector('i');
if (icon) {
icon.classList.remove('bi-chevron-down');
icon.classList.add('bi-chevron-right');
}
});
}
});
});
</script>
{% endblock script %}