1387 lines
95 KiB
HTML
1387 lines
95 KiB
HTML
{% extends "base.html" %}
|
||
{% load static %}
|
||
{% load tz %}
|
||
{% load i18n %}
|
||
{% load documents_tags %}
|
||
{% load asset_tags %}
|
||
{% load document_viewer_tags %}
|
||
|
||
{% block content %}
|
||
<style>
|
||
/* Pull top-right controls to the top and leave margin below orthophotos button */
|
||
#map .maplibregl-ctrl-top-right {
|
||
top: 10px !important;
|
||
}
|
||
.layer-switcher-ctrl {
|
||
margin-bottom: 15px !important;
|
||
}
|
||
</style>
|
||
<div class="row mt-2" style="height: calc(100vh - 200px);">
|
||
<div class="col-md-8" style="max-height: calc(100vh - 200px); overflow: auto;">
|
||
<div class="d-flex align-items-center justify-content-between mb-0">
|
||
<h3 class="h3 mb-0">
|
||
<i class="bi bi-sign-stop-lights"></i><span class="mx-2">{{ intersection.code}}</span> - <span class="mx-2">{{ intersection.get_name }}</span>
|
||
</h3>
|
||
{% if can_edit_location %}
|
||
<div class="d-flex gap-2">
|
||
<button class="btn btn-sm btn-outline-primary"
|
||
data-location-edit="{{ intersection.pk }}"
|
||
title="{% translate 'Modifier cette intersection' %}">
|
||
<i class="bi bi-pencil-square"></i>
|
||
</button>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
<hr>
|
||
<div class="row mb-4">
|
||
<div class="col-md-8">
|
||
<label class="mx-2">{% translate "Statut" %}:</label><i class="bi bi-circle-fill status_{{ intersection.status }}"></i> {{ intersection.get_status_display }}<br>
|
||
<label class="mx-2">{% translate "Tension (Voltage)" %}:</label>{{ intersection.get_voltage_type_display }}<br>
|
||
<div class="my-2">
|
||
<label class="mx-2">{% translate "Connexion centrale" %}:</label>
|
||
{% if intersection.is_connected_to_control_center %}
|
||
<i class="bi bi-check-square"></i>
|
||
{% elif intersection.is_connected_to_control_center == False %}
|
||
<i class="bi bi-x-square"></i>
|
||
{% else %}
|
||
<i class="bi bi-question-square"></i>
|
||
{% endif %}
|
||
<br>
|
||
<label class="mx-2">{% translate "Index" %}:</label>{{ intersection.current_index|default:""}}
|
||
{% if intersection.master %}
|
||
<br>
|
||
<label class="mx-2">{% translate "Carrefour principal" %}:</label><a href="{% url 'assets:trafficlights_intersections_detail' intersection.master.id %}">{{ intersection.master.code|default:"/" }}</a>
|
||
{% endif %}
|
||
</div>
|
||
{% if can_edit_location %}
|
||
<div class="my-2 d-flex align-items-start flex-wrap gap-1">
|
||
<label class="mx-2 me-1">{% translate "Contrats" %}:</label>
|
||
<span class="contract-badges-container d-flex flex-wrap gap-1 align-items-center">
|
||
{% if trafficlight_contracts %}
|
||
{% for tlc in trafficlight_contracts %}
|
||
<span class="badge bg-secondary me-1" title="{{ tlc.get_maintenance_type_display }} — {{ tlc.get_status_display }}">
|
||
{{ tlc.contract.contract_number }}
|
||
<span class="text-light opacity-75 ms-1">({{ tlc.get_maintenance_type_display }})</span>
|
||
</span>
|
||
{% endfor %}
|
||
{% else %}
|
||
<span class="text-muted fst-italic">{% translate "Aucun" %}</span>
|
||
{% endif %}
|
||
</span>
|
||
<button class="btn btn-sm btn-link p-0 ms-1" data-bs-toggle="modal" data-bs-target="#contractsEditModal"
|
||
title="{% translate 'Modifier les contrats' %}">
|
||
<i class="bi bi-pencil-square text-primary"></i>
|
||
</button>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="me-2 mb-2">{% translate "Commune" %}:</label>{{ intersection.municipality }}<br>
|
||
{% if intersection.lon %}
|
||
<a href="https://www.google.com/maps?cbll={{ intersection.lat|floatformat:'6u'}},{{ intersection.lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid=" target="_blank" style="text-decoration: none; font-size: 16px; display: flex; align-items: center;">
|
||
<img src="{% static 'common/google-street-view-logo.png' %}" alt="{% translate 'Street View' %}" height="20" style="margin-right: 8px;">
|
||
{% translate "View in Google Street View" %}
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if staged_assets_count > 0 %}
|
||
<div class="card border-warning mb-4 shadow-sm" style="background: linear-gradient(135deg, #fffbeb, #fef3c7); border-left: 5px solid #f59e0b;">
|
||
<div class="card-body">
|
||
<h5 class="card-title text-warning-emphasis d-flex align-items-center gap-2 font-weight-bold">
|
||
<i class="bi bi-exclamation-triangle-fill text-warning fs-4"></i>
|
||
{% translate "Assets importés par DXF en attente de validation" %}
|
||
</h5>
|
||
<p class="card-text text-secondary mb-3">
|
||
{% blocktranslate count count=staged_assets_count %}
|
||
Il y a <strong>{{ count }}</strong> poteau importé depuis un plan DXF qui est en attente de validation pour ce carrefour.
|
||
{% plural %}
|
||
Il y a <strong>{{ count }}</strong> poteaux importés depuis un plan DXF qui sont en attente de validation pour ce carrefour.
|
||
{% endblocktranslate %}
|
||
</p>
|
||
|
||
<!-- Date of installation selection -->
|
||
<div class="mb-3 d-flex align-items-center gap-2" style="max-width: 320px;">
|
||
<label for="installation_date_picker" class="form-label font-weight-semibold text-dark small mb-0 flex-shrink-0">
|
||
<i class="bi bi-calendar-event text-primary me-1"></i>
|
||
{% translate "Date d'installation :" %}
|
||
</label>
|
||
<input type="date" id="installation_date_picker" class="form-control form-control-sm" value="{{ today_date }}">
|
||
</div>
|
||
|
||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||
<form method="post" action="{% url 'assets:integrate_dxf' intersection.id %}" class="d-inline integrate-form">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="choice" value="choice_a">
|
||
<input type="hidden" name="installation_date" class="form-installation-date" value="{{ today_date }}">
|
||
<button type="submit" class="btn btn-warning btn-sm font-weight-semibold shadow-sm text-dark px-3 py-1.5 rounded-2">
|
||
<i class="bi bi-arrow-repeat me-1"></i> {% translate "Option A : Remplacer par code et créer les nouveaux" %}
|
||
</button>
|
||
</form>
|
||
|
||
<form method="post" action="{% url 'assets:integrate_dxf' intersection.id %}" class="d-inline integrate-form">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="choice" value="choice_b">
|
||
<input type="hidden" name="installation_date" class="form-installation-date" value="{{ today_date }}">
|
||
<button type="submit" class="btn btn-outline-danger btn-sm font-weight-semibold px-3 py-1.5 rounded-2" onclick="return confirm('{% translate "Êtes-vous sûr de vouloir archiver TOUS les poteaux existants de ce carrefour et d\'activer uniquement les nouveaux poteaux importés ?" %}');">
|
||
<i class="bi bi-trash-fill me-1"></i> {% translate "Option B : Remplacement complet (Archiver tout le reste)" %}
|
||
</button>
|
||
</form>
|
||
|
||
<a href="{% url 'assets:import_dxf' intersection.id %}" class="btn btn-outline-secondary btn-sm ms-auto px-3 py-1.5 rounded-2">
|
||
<i class="bi bi-pencil-square"></i> {% translate "Réimporter / Corriger" %}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const datePicker = document.getElementById('installation_date_picker');
|
||
if (datePicker) {
|
||
datePicker.addEventListener('change', function() {
|
||
document.querySelectorAll('.form-installation-date').forEach(input => {
|
||
input.value = datePicker.value;
|
||
});
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
{% endif %}
|
||
|
||
<!-- Documents -->
|
||
<div class="card mb-4" style="border-color: #b2d8d1;">
|
||
<div class="card-header d-flex justify-content-between align-items-center" style="background: linear-gradient(to left, #d0eeea, #f5fcfb); border-color: #b2d8d1;">
|
||
<h4>{% translate "Documents" %}</h4>
|
||
<button onclick="" class="btn btn-light border-0" title="{% translate 'Ajouter' %}" data-bs-toggle="modal" data-bs-target="#addDocumentModal">
|
||
<i class="bi bi-plus-circle text-primary"></i>
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
{% if can_browse_folder and document_folder %}
|
||
<div class="row">
|
||
<div class="col-md-12 mb-3">
|
||
<label class="mx-2">{% translate "Répertoire principal" %}:</label>
|
||
<span class="mx-2"><a href="/documents/?folder={{ document_folder.slug }}">{{ intersection.code }}</a></span>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{# ── Document grid ── #}
|
||
{% if use_managed_documents %}
|
||
{# Nouveau système : le composant gère sa propre grille + modale visionneuse #}
|
||
{% render_document_viewer main_documents modal_id="tliDocViewerModal" show_tags=True show_versions=True location_id=intersection.id %}
|
||
{% else %}
|
||
{# Ancien système - TrafficLightIntersectionDocument #}
|
||
<div class="row">
|
||
{% for document in main_documents %}
|
||
{% if document %}
|
||
<div class="col-md-3 mb-3">
|
||
<div class="card h-100 shadow-sm border rounded-3">
|
||
<div class="d-flex">
|
||
<!-- Colonne gauche : contenu document -->
|
||
<div class="flex-grow-1 p-2" style="min-width:0; overflow:hidden;">
|
||
<div class="d-flex flex-column align-items-center mb-2">
|
||
<!-- Badge centré -->
|
||
<div class="text-muted" style="font-size: 0.85em;">
|
||
<span class="badge bg-primary">{{ document.get_document_type_display }}</span>
|
||
</div>
|
||
|
||
<!-- Image -->
|
||
<a href="{{ document.file.url }}" target="_blank" class="text-decoration-none w-100" title="{{ document.filename }}">
|
||
<div class="img-thumbnail d-flex justify-content-center align-items-center mt-1" style="width: 100%; height: 100px; overflow: hidden; background-color: #f8f9fa;">
|
||
{% if document.thumbnail %}
|
||
<img src="{{ document.thumbnail.url }}" alt="{{ document.filename }}"
|
||
style="max-height: 100%; max-width: 100%; object-fit: contain;">
|
||
{% else %}
|
||
<i class="{{ document.filename|file_icon_class }}" style="font-size: 2.5rem;"></i>
|
||
{% endif %}
|
||
</div>
|
||
</a>
|
||
</div>
|
||
|
||
<div class="text-muted text-start" style="font-size: 0.85em;">
|
||
{{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}<br>
|
||
{{ document.uploaded_by.username }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Colonne droite : icônes -->
|
||
<div class="d-flex flex-column justify-content-end align-items-center p-2" style="width: 40px; min-height: 100%;">
|
||
<!-- Historique -->
|
||
<button class="btn btn-sm p-0 border-0 bg-transparent mb-2"
|
||
data-bs-toggle="modal"
|
||
data-bs-target="#documentHistoryModal"
|
||
data-document-type="{{ document.document_type }}"
|
||
data-location-id="{{ intersection.id }}"
|
||
title="{% translate 'Historique' %}">
|
||
<i class="bi bi-clock-history text-primary"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Données JSON pour JavaScript -->
|
||
<script type="application/json" id="documentConfigData">
|
||
{
|
||
"use_managed_documents": {% if use_managed_documents %}true{% else %}false{% endif %},
|
||
"can_add_unique_tag_documents": {% if can_add_unique_tag_documents %}true{% else %}false{% endif %},
|
||
"config_url": "{% url 'assets:trafficlights_intersections_document_config' intersection.id %}"
|
||
}
|
||
</script>
|
||
<script type="application/json" id="georeferencedDocumentsData">
|
||
{{ georeferenced_documents_json|safe }}
|
||
</script>
|
||
<!-- Modal form add document -->
|
||
<div class="modal fade" id="addDocumentModal" tabindex="-1" aria-labelledby="addDocumentModalLabel" aria-hidden="true" style="z-index: 10050;">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="addDocumentModalLabel">{% translate "Ajouter des documents" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="addDocumentForm" method="post" enctype="multipart/form-data" action="{% url 'assets:trafficlights_intersections_add_document' intersection.id %}">
|
||
{% csrf_token %}
|
||
|
||
<div class="mb-3">
|
||
<label for="fileInput" class="form-label">{% translate "Sélectionner des fichiers" %}</label>
|
||
<input type="file" id="fileInput" name="files" multiple class="form-control">
|
||
</div>
|
||
|
||
<div id="fileFieldsContainer">
|
||
<!-- Champs dynamiques injectés ici -->
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="submit" class="btn btn-primary" id="documentSubmitButton">
|
||
{% translate "Ajouter" %}
|
||
<span class="spinner-border spinner-border-sm ms-2 d-none" role="status" aria-hidden="true" id="documentSubmitSpinner"></span>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Modal form delete document -->
|
||
<div class="modal fade" id="documentDeleteModal" tabindex="-1" aria-labelledby="documentDeleteModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content">
|
||
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="documentDeleteModalLabel">{% translate "Confirmer la suppression" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<p>{% translate "Voulez-vous vraiment supprimer ce document ?" %}</p>
|
||
<p><strong id="deleteDocumentFileName">{% translate "Nom du fichier" %}</strong></p>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<form method="post" id="deleteDocumentForm" action="">
|
||
{% csrf_token %}
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||
<button type="submit" class="btn btn-danger">{% translate "Supprimer" %}</button>
|
||
</form>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Modal for document history -->
|
||
<div class="modal fade" id="documentHistoryModal" tabindex="-1" aria-labelledby="documentHistoryModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="documentHistoryModalLabel">{% translate "Historique des documents" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="documentHistoryContent" class="row g-3">
|
||
<!-- Contenu dynamique injecté ici -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div class="card mb-4" id="location-detail-container"
|
||
style="border-color: #cce0ff;"
|
||
data-thematic-code="{{ thematic_code }}"
|
||
data-location-type="{{ location_type }}"
|
||
data-location-id="{{ intersection.id }}">
|
||
<div class="card-header d-flex justify-content-between align-items-center" style="background: linear-gradient(to left, #c8dffe, #f0f8ff); border-color: #cce0ff;">
|
||
<h4 data-bs-toggle="collapse" href="#assets_card_body" role="button" class="mb-0" style="color: #1e40af;"><i class="bi bi-boxes me-2"></i>{% translate "Assets" %}</h4>
|
||
{% if allowed_location_actions %}
|
||
<div class="d-flex gap-2">
|
||
{% if 'create' in allowed_location_actions %}
|
||
<button id="add-asset-btn" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addAssetModal" title="{% translate 'Ajouter un asset' %}">
|
||
<i class="bi bi-plus-circle"></i> {% translate "Ajouter" %}
|
||
</button>
|
||
{% endif %}
|
||
{% if can_edit_location %}
|
||
<a href="{% url 'assets:import_dxf' intersection.id %}" class="btn btn-sm btn-outline-success" title="{% translate 'Importer plan DXF' %}">
|
||
<i class="bi bi-file-earmark-arrow-up"></i> {% translate "Importer plan DXF" %}
|
||
</a>
|
||
<a href="{% url 'assets:import_cross_plan' intersection.id %}" class="btn btn-sm btn-outline-info" title="{% translate 'Importer plan de croix' %}">
|
||
<i class="bi bi-file-earmark-excel"></i> {% translate "Importer plan de croix" %}
|
||
</a>
|
||
{% endif %}
|
||
{% if 'bulk_archive' in allowed_location_actions %}
|
||
<button id="bulk-archive-btn" class="btn btn-sm btn-outline-danger" title="{% translate 'Archiver tous les assets' %}">
|
||
<i class="bi bi-archive"></i> {% translate "Tout archiver" %}
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body collapse show" id="assets_card_body" style="background-color: #f7fbff;">
|
||
<style>
|
||
#assetsTab .nav-link {
|
||
color: #495057;
|
||
padding: 0.6rem 1.1rem;
|
||
border-radius: 0.5rem 0.5rem 0 0;
|
||
border: 1px solid #dee2e6;
|
||
border-bottom: none;
|
||
background: #f8f9fa;
|
||
font-weight: 500;
|
||
transition: background 0.15s, box-shadow 0.15s;
|
||
}
|
||
#assetsTab .nav-link:hover:not(.active) {
|
||
background: #e9ecef;
|
||
color: #212529;
|
||
}
|
||
#assetsTab .nav-link.active {
|
||
color: #0d6efd;
|
||
background: #fff;
|
||
border-color: #dee2e6 #dee2e6 #fff;
|
||
box-shadow: 0 -2px 6px rgba(0,0,0,0.08);
|
||
font-weight: 600;
|
||
}
|
||
/* Assets inactifs / archivés */
|
||
.asset-archived {
|
||
opacity: 0.55;
|
||
}
|
||
.asset-archived a {
|
||
text-decoration: line-through !important;
|
||
color: #6c757d !important;
|
||
}
|
||
.asset-inactive {
|
||
opacity: 0.7;
|
||
}
|
||
.asset-inactive a {
|
||
color: #6c757d !important;
|
||
}
|
||
</style>
|
||
<ul class="nav nav-tabs" id="assetsTab" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active d-flex align-items-center gap-2"
|
||
id="assets-outdoor-tab" data-bs-toggle="tab" data-bs-target="#assets-outdoor"
|
||
type="button" role="tab" aria-controls="assets-outdoor" aria-selected="true">
|
||
<i class="bi bi-sign-stop-lights"></i>
|
||
<span>{% translate "Installation extérieure" %}</span>
|
||
</button>
|
||
</li>
|
||
{% if not intersection.is_radar %}
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link d-flex align-items-center gap-2"
|
||
id="assets-controller-tab" data-bs-toggle="tab" data-bs-target="#assets-controller"
|
||
type="button" role="tab" aria-controls="assets-controller" aria-selected="false">
|
||
<i class="bi bi-cpu"></i>
|
||
<span>{% translate "Contrôleur" %}</span>
|
||
</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link d-flex align-items-center gap-2"
|
||
id="assets-programmation-tab" data-bs-toggle="tab" data-bs-target="#assets-programmation"
|
||
type="button" role="tab" aria-controls="assets-programmation" aria-selected="false">
|
||
<i class="bi bi-kanban"></i>
|
||
<span>{% translate "Programmation" %}</span>
|
||
</button>
|
||
</li>
|
||
{% endif %}
|
||
</ul>
|
||
<div class="tab-content border border-top-0 rounded-bottom p-3" id="assetsTabContent" style="background-color: #fff;">
|
||
|
||
{# ── Onglet Installation extérieure ── #}
|
||
<div class="tab-pane fade show active" id="assets-outdoor" role="tabpanel" aria-labelledby="assets-outdoor-tab">
|
||
<div class="row g-3">
|
||
|
||
{# ── Colonne Poteaux ── #}
|
||
<div class="col-md-6">
|
||
<div class="card h-100 border-0 shadow-sm">
|
||
<div class="card-header bg-light d-flex align-items-center gap-2 py-2">
|
||
<i class="bi bi-sign-stop-lights text-secondary"></i>
|
||
<span class="fw-semibold">{% translate "Poteaux" %}</span>
|
||
<span class="badge bg-secondary ms-auto">{{ assets.poles|length }}</span>
|
||
<button id="expandAllPoles" class="btn btn-sm btn-link p-0 text-secondary border-0 flex-shrink-0"
|
||
type="button"
|
||
title="{% translate 'Tout déplier / replier' %}">
|
||
<i class="bi bi-chevron-expand small"></i>
|
||
</button>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
{% if assets.poles %}
|
||
<ul class="list-group list-group-flush" id="accordionPoles">
|
||
{% for pole in assets.poles %}
|
||
<li class="list-group-item p-0{% if pole.status == 'archived' %} asset-archived{% elif pole.status == 'inactive' %} asset-inactive{% endif %}">
|
||
{# ── En-tête du poteau ── #}
|
||
<div class="d-flex align-items-center px-3 py-2 gap-2">
|
||
{% if pole.sorted_radars and not pole.sorted_lanterns and not pole.sorted_accessories %}
|
||
<i class="bi bi-radar text-secondary small flex-shrink-0"></i>
|
||
{% else %}
|
||
<i class="bi bi-feather2 text-secondary small flex-shrink-0"></i>
|
||
{% endif %}
|
||
<a href="{{ pole.get_absolute_url }}" class="fw-medium text-decoration-none flex-grow-0">{{ pole.code }}</a>
|
||
{% if pole.name %}<span class="text-muted small">{{ pole.name }}</span>{% endif %}
|
||
{% if pole.status != 'active' %}<span class="badge {% if pole.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-1" title="{{ pole.get_status_display }}">{{ pole.get_status_display }}</span>{% endif %}
|
||
{% if pole.sorted_lanterns or pole.sorted_accessories or pole.sorted_radars or pole.sorted_detectors or pole.sorted_signpanels %}
|
||
<div class="d-flex align-items-center gap-1 ms-auto">
|
||
{% if pole.sorted_accessories %}
|
||
<span class="badge bg-success bg-opacity-10 text-success" title="{% translate 'Accessoires' %}">{{ pole.sorted_accessories|length }} <i class="bi bi-align-middle"></i></span>
|
||
{% endif %}
|
||
{% if pole.sorted_lanterns %}
|
||
<span class="badge bg-primary bg-opacity-10 text-primary" title="{% translate 'Lanternes' %}">{{ pole.sorted_lanterns|length }} <i class="bi bi-stoplights"></i></span>
|
||
{% endif %}
|
||
{% if pole.sorted_radars %}
|
||
<span class="badge bg-info bg-opacity-10 text-info" title="{% translate 'Radars' %}">{{ pole.sorted_radars|length }} <i class="bi bi-radar"></i></span>
|
||
{% endif %}
|
||
{% if pole.sorted_detectors %}
|
||
<span class="badge bg-secondary bg-opacity-10 text-secondary" title="{% translate 'Détecteurs' %}">{{ pole.sorted_detectors|length }} <i class="bi bi-radar"></i></span>
|
||
{% endif %}
|
||
{% if pole.sorted_signpanels %}
|
||
<span class="badge bg-warning bg-opacity-10 text-warning" title="{% translate 'Panneaux de signalisation' %}">{{ pole.sorted_signpanels|length }} <i class="bi bi-signpost"></i></span>
|
||
{% endif %}
|
||
</div>
|
||
<button class="btn btn-sm btn-link p-0 text-secondary border-0 flex-shrink-0"
|
||
type="button"
|
||
data-bs-toggle="collapse"
|
||
data-bs-target="#poleCollapse{{ pole.id }}"
|
||
aria-expanded="false"
|
||
aria-controls="poleCollapse{{ pole.id }}"
|
||
title="{% translate 'Afficher les lanternes, accessoires, radars, détecteurs et panneaux de signalisation' %}">
|
||
<i class="bi bi-chevron-down small"></i>
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
{# ── Lanternes + Accessoires + Radars + Détecteurs + Panneaux ── #}
|
||
{% if pole.sorted_lanterns or pole.sorted_accessories or pole.sorted_radars or pole.sorted_detectors or pole.sorted_signpanels %}
|
||
<div id="poleCollapse{{ pole.id }}" class="collapse">
|
||
<ul class="list-group list-group-flush border-top">
|
||
{% for lantern in pole.sorted_lanterns %}
|
||
<li class="list-group-item py-1 ps-5 pe-3 d-flex align-items-center gap-2 bg-light{% if lantern.status == 'archived' %} asset-archived{% elif lantern.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-stoplights text-warning small"></i>
|
||
<a href="{{ lantern.get_absolute_url }}" class="text-decoration-none small fw-medium">{{ lantern.code }}</a>
|
||
{% if lantern.get_name %}<span class="text-muted small fst-italic">{{ lantern.get_name }}</span>{% endif %}
|
||
{% if lantern.status != 'active' %}<span class="badge {% if lantern.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-auto">{{ lantern.get_status_display }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
{% for accessory in pole.sorted_accessories %}
|
||
<li class="list-group-item py-1 ps-5 pe-3 d-flex align-items-center gap-2 bg-light{% if accessory.status == 'archived' %} asset-archived{% elif accessory.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-align-middle text-success small"></i>
|
||
<a href="{{ accessory.get_absolute_url }}" class="text-decoration-none small fw-medium">{{ accessory.code }}</a>
|
||
{% if accessory.get_name %}<span class="text-muted small fst-italic">{{ accessory.get_name }}</span>{% endif %}
|
||
{% if accessory.status != 'active' %}<span class="badge {% if accessory.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-auto">{{ accessory.get_status_display }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
{% for radar in pole.sorted_radars %}
|
||
<li class="list-group-item py-1 ps-5 pe-3 d-flex align-items-center gap-2 bg-light{% if radar.status == 'archived' %} asset-archived{% elif radar.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-radar text-info small"></i>
|
||
<a href="{{ radar.get_absolute_url }}" class="text-decoration-none small fw-medium">{{ radar.code }}</a>
|
||
{% if radar.get_name %}<span class="text-muted small fst-italic">{{ radar.get_name }}</span>{% endif %}
|
||
{% if radar.status != 'active' %}<span class="badge {% if radar.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-auto">{{ radar.get_status_display }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
{% for detector in pole.sorted_detectors %}
|
||
<li class="list-group-item py-1 ps-5 pe-3 d-flex align-items-center gap-2 bg-light{% if detector.status == 'archived' %} asset-archived{% elif detector.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-radar text-secondary small"></i>
|
||
<a href="{{ detector.get_absolute_url }}" class="text-decoration-none small fw-medium">{{ detector.code }}</a>
|
||
{% if detector.get_name %}<span class="text-muted small fst-italic">{{ detector.get_name }}</span>{% endif %}
|
||
{% if detector.status != 'active' %}<span class="badge {% if detector.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-auto">{{ detector.get_status_display }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
{% for panel in pole.sorted_signpanels %}
|
||
<li class="list-group-item py-1 ps-5 pe-3 d-flex align-items-center gap-2 bg-light{% if panel.status == 'archived' %} asset-archived{% elif panel.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-signpost text-warning small"></i>
|
||
<a href="{{ panel.get_absolute_url }}" class="text-decoration-none small fw-medium">{{ panel.code }}</a>
|
||
{% if panel.signpanel_type %}<span class="text-muted small">{{ panel.signpanel_type }}</span>{% endif %}
|
||
{% if panel.signpanel_text %}<span class="text-muted small fst-italic">"{{ panel.signpanel_text }}"</span>{% endif %}
|
||
{% if panel.status != 'active' %}<span class="badge {% if panel.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-auto">{{ panel.get_status_display }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="text-muted small p-3 mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucun poteau" %}</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── Colonne Armoires + Câbles ── #}
|
||
<div class="col-md-6 d-flex flex-column gap-3">
|
||
|
||
{# Armoires électriques #}
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-header bg-light d-flex align-items-center gap-2 py-2">
|
||
<i class="bi bi-lightning-charge text-secondary"></i>
|
||
<span class="fw-semibold">{% translate "Armoires électriques" %}</span>
|
||
<span class="badge bg-secondary ms-auto">{{ assets.electrical_cabinets|length }}</span>
|
||
</div>
|
||
{% if assets.electrical_cabinets %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for cabinet in assets.electrical_cabinets %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2{% if cabinet.status == 'archived' %} asset-archived{% elif cabinet.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-box text-secondary small"></i>
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightelectricalcabinet' cabinet.id %}" class="fw-medium text-decoration-none">{{ cabinet.code }}</a>
|
||
<span class="text-muted small ms-1">{{ cabinet.get_name }}</span>
|
||
{% if cabinet.status != 'active' %}<span class="badge {% if cabinet.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-auto">{{ cabinet.get_status_display }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<div class="card-body py-2">
|
||
<p class="text-muted small mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucune armoire électrique" %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{# Câbles #}
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-header bg-light d-flex align-items-center gap-2 py-2">
|
||
<i class="bi bi-ethernet text-secondary"></i>
|
||
<span class="fw-semibold">{% translate "Câbles" %}</span>
|
||
<span class="badge bg-secondary ms-auto">{{ assets.cables|length }}</span>
|
||
</div>
|
||
{% if assets.cables %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for cable in assets.cables %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2{% if cable.status == 'archived' %} asset-archived{% elif cable.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<svg width="24" height="12" class="shadow-sm rounded-1" style="vertical-align: middle; background-color: rgba(255,255,255,0.7); border: 1px solid rgba(0,0,0,0.08);" title="{% translate 'Couleur du câble sur la carte' %}">
|
||
<line x1="2" y1="6" x2="22" y2="6" stroke="{{ cable.color|default:'#e53935' }}" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="4,3" />
|
||
</svg>
|
||
<i class="bi bi-plug text-secondary small"></i>
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightcable' cable.id %}" class="fw-medium text-decoration-none">{{ cable.code }}</a>
|
||
{% if cable.name %}<span class="text-muted small ms-1">{{ cable.name }}</span>{% endif %}
|
||
{% if cable.status != 'active' %}<span class="badge {% if cable.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-auto">{{ cable.get_status_display }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<div class="card-body py-2">
|
||
<p class="text-muted small mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucun câble" %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{# Radars #}
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-header bg-light d-flex align-items-center gap-2 py-2">
|
||
<i class="bi bi-radar text-secondary"></i>
|
||
<span class="fw-semibold">{% translate "Radars préventifs" %}</span>
|
||
<span class="badge bg-secondary ms-auto">{{ assets.radars|length }}</span>
|
||
</div>
|
||
{% if assets.radars %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for radar in assets.radars %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2{% if radar.status == 'archived' %} asset-archived{% elif radar.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-broadcast text-secondary small"></i>
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightradar' radar.id %}" class="fw-medium text-decoration-none">{{ radar.code }}</a>
|
||
{% if radar.get_name %}<span class="text-muted small ms-1">{{ radar.get_name }}</span>{% endif %}
|
||
{% if radar.status != 'active' %}<span class="badge {% if radar.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %} ms-auto">{{ radar.get_status_display }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<div class="card-body py-2">
|
||
<p class="text-muted small mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucun radar préventif" %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
</div>{# /col armoires+câbles #}
|
||
</div>{# /row #}
|
||
</div>
|
||
|
||
{% if not intersection.is_radar %}
|
||
{# ── Onglet Contrôleur ── #}
|
||
<div class="tab-pane fade" id="assets-controller" role="tabpanel" aria-labelledby="assets-controller-tab">
|
||
{% if assets.controllers.first %}
|
||
{% with ctrl=assets.controllers.first %}
|
||
|
||
{# ── Carte contrôleur ── #}
|
||
<div class="card border-0 shadow-sm mb-3">
|
||
<div class="card-header bg-light d-flex align-items-center gap-2 py-2">
|
||
<i class="bi bi-cpu text-secondary"></i>
|
||
<span class="fw-semibold">{% translate "Contrôleur de trafic" %}</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="d-flex align-items-center gap-3 mb-3">
|
||
<i class="bi bi-cpu-fill fs-2 text-secondary opacity-50"></i>
|
||
<div>
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightcontroller' ctrl.id %}" class="fw-semibold fs-6 text-decoration-none">{{ ctrl.code }}</a>
|
||
{% if ctrl.name %}<div class="text-muted small">{{ ctrl.name }}</div>{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="row g-2 small">
|
||
<div class="col-sm-6">
|
||
<span class="text-muted">{% translate "Modèle" %} :</span>
|
||
<span class="ms-1">{% if ctrl.model %}{{ ctrl.model.get_name }}{% else %}<span class="text-muted fst-italic">—</span>{% endif %}</span>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<span class="text-muted">{% translate "Type" %} :</span>
|
||
<span class="ms-1">{{ ctrl.cabinet_type|default:"—" }}</span>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<span class="text-muted">{% translate "Plan" %} :</span>
|
||
<span class="ms-1">{{ ctrl.plan|default:"—" }}</span>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<span class="text-muted">{% translate "Puissance" %} :</span>
|
||
<span class="ms-1">{{ ctrl.power|default:"—" }}</span>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<span class="text-muted">{% translate "Date de mise en service" %} :</span>
|
||
<span class="ms-1">{% if ctrl.installation_date %}{{ ctrl.installation_date|date:"d/m/Y" }}{% else %}<span class="fst-italic">—</span>{% endif %}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── Accordéons liés au contrôleur ── #}
|
||
<div class="accordion" id="controllerAccordion{{ ctrl.id }}">
|
||
|
||
{# ── Block 1 : Hardwares du contrôleur ── #}
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header" id="headingHardware{{ ctrl.id }}">
|
||
<button class="accordion-button collapsed" type="button"
|
||
data-bs-toggle="collapse"
|
||
data-bs-target="#collapseHardware{{ ctrl.id }}"
|
||
aria-expanded="false"
|
||
aria-controls="collapseHardware{{ ctrl.id }}">
|
||
<i class="bi bi-motherboard me-2 text-secondary"></i>
|
||
{% translate "Hardwares du contrôleur" %}
|
||
<span class="badge bg-secondary ms-2">{{ ctrl.hardwares.count }}</span>
|
||
</button>
|
||
</h2>
|
||
<div id="collapseHardware{{ ctrl.id }}" class="accordion-collapse collapse"
|
||
aria-labelledby="headingHardware{{ ctrl.id }}"
|
||
data-bs-parent="#controllerAccordion{{ ctrl.id }}">
|
||
<div class="accordion-body p-0">
|
||
{% with hw_list=ctrl.sorted_hardwares %}
|
||
{% if hw_list %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for hw in hw_list %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2{% if hw.status == 'archived' %} asset-archived{% elif hw.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-motherboard text-secondary small"></i>
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightcontrollerhardware' hw.id %}" class="fw-medium text-decoration-none">{{ hw.code }}</a>
|
||
{% if hw.name %}<span class="text-muted small ms-1">{{ hw.name }}</span>{% endif %}
|
||
<span class="ms-auto d-flex gap-1 align-items-center">
|
||
{% if hw.hardware %}<span class="badge bg-light text-dark border">{{ hw.hardware }}</span>{% endif %}
|
||
{% if hw.status != 'active' %}<span class="badge {% if hw.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %}">{{ hw.get_status_display }}</span>{% endif %}
|
||
</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="text-muted small p-3 mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucun hardware" %}</p>
|
||
{% endif %}
|
||
{% endwith %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── Block 2 : Détecteurs ── #}
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header" id="headingDetectors{{ ctrl.id }}">
|
||
<button class="accordion-button collapsed" type="button"
|
||
data-bs-toggle="collapse"
|
||
data-bs-target="#collapseDetectors{{ ctrl.id }}"
|
||
aria-expanded="false"
|
||
aria-controls="collapseDetectors{{ ctrl.id }}">
|
||
<i class="bi bi-radar me-2 text-secondary"></i>
|
||
{% translate "Détecteurs" %}
|
||
<span class="badge bg-secondary ms-2">{{ ctrl.detectors.count }}</span>
|
||
</button>
|
||
</h2>
|
||
<div id="collapseDetectors{{ ctrl.id }}" class="accordion-collapse collapse"
|
||
aria-labelledby="headingDetectors{{ ctrl.id }}"
|
||
data-bs-parent="#controllerAccordion{{ ctrl.id }}">
|
||
<div class="accordion-body p-0">
|
||
{% with det_list=ctrl.sorted_detectors %}
|
||
{% if det_list %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for det in det_list %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2{% if det.status == 'archived' %} asset-archived{% elif det.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-radar text-secondary small"></i>
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightdetector' det.id %}" class="fw-medium text-decoration-none">{{ det.code }}</a>
|
||
{% if det.name %}<span class="text-muted small ms-1">{{ det.name }}</span>{% endif %}
|
||
<span class="ms-auto d-flex gap-1 align-items-center">
|
||
{% if det.hardware %}<span class="badge bg-light text-dark border">{{ det.hardware }}</span>{% endif %}
|
||
{% if det.status != 'active' %}<span class="badge {% if det.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %}">{{ det.get_status_display }}</span>{% endif %}
|
||
</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="text-muted small p-3 mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucun détecteur" %}</p>
|
||
{% endif %}
|
||
{% endwith %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── Block 3 : Hardware réseau ── #}
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header" id="headingNetwork{{ ctrl.id }}">
|
||
<button class="accordion-button collapsed" type="button"
|
||
data-bs-toggle="collapse"
|
||
data-bs-target="#collapseNetwork{{ ctrl.id }}"
|
||
aria-expanded="false"
|
||
aria-controls="collapseNetwork{{ ctrl.id }}">
|
||
<i class="bi bi-hdd-network me-2 text-secondary"></i>
|
||
{% translate "Hardware réseau" %}
|
||
<span class="badge bg-secondary ms-2">{{ ctrl.network_hardwares.count }}</span>
|
||
</button>
|
||
</h2>
|
||
<div id="collapseNetwork{{ ctrl.id }}" class="accordion-collapse collapse"
|
||
aria-labelledby="headingNetwork{{ ctrl.id }}"
|
||
data-bs-parent="#controllerAccordion{{ ctrl.id }}">
|
||
<div class="accordion-body p-0">
|
||
{% with net_list=ctrl.sorted_network_hardwares %}
|
||
{% if net_list %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for net in net_list %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2{% if net.status == 'archived' %} asset-archived{% elif net.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-hdd-network text-secondary small"></i>
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightnetworkhardware' net.id %}" class="fw-medium text-decoration-none">{{ net.code }}</a>
|
||
{% if net.name %}<span class="text-muted small ms-1">{{ net.name }}</span>{% endif %}
|
||
<span class="ms-auto d-flex gap-1 align-items-center">
|
||
{% if net.hardware %}<span class="badge bg-light text-dark border">{{ net.hardware }}</span>{% endif %}
|
||
{% if net.status != 'active' %}<span class="badge {% if net.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %}">{{ net.get_status_display }}</span>{% endif %}
|
||
</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="text-muted small p-3 mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucun hardware réseau" %}</p>
|
||
{% endif %}
|
||
{% endwith %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── Block 4 : Hardware Tension ── #}
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header" id="headingVoltage{{ ctrl.id }}">
|
||
<button class="accordion-button collapsed" type="button"
|
||
data-bs-toggle="collapse"
|
||
data-bs-target="#collapseVoltage{{ ctrl.id }}"
|
||
aria-expanded="false"
|
||
aria-controls="collapseVoltage{{ ctrl.id }}">
|
||
<i class="bi bi-lightning-charge me-2 text-secondary"></i>
|
||
{% translate "Hardware Tension" %}
|
||
<span class="badge bg-secondary ms-2">{{ ctrl.voltage_hardwares.count }}</span>
|
||
</button>
|
||
</h2>
|
||
<div id="collapseVoltage{{ ctrl.id }}" class="accordion-collapse collapse"
|
||
aria-labelledby="headingVoltage{{ ctrl.id }}"
|
||
data-bs-parent="#controllerAccordion{{ ctrl.id }}">
|
||
<div class="accordion-body p-0">
|
||
{% with volt_list=ctrl.sorted_voltage_hardwares %}
|
||
{% if volt_list %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for volt in volt_list %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2{% if volt.status == 'archived' %} asset-archived{% elif volt.status == 'inactive' %} asset-inactive{% endif %}">
|
||
<i class="bi bi-lightning-charge text-secondary small"></i>
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightvoltagehardware' volt.id %}" class="fw-medium text-decoration-none">{{ volt.code }}</a>
|
||
{% if volt.name %}<span class="text-muted small ms-1">{{ volt.name }}</span>{% endif %}
|
||
<span class="ms-auto d-flex gap-1 align-items-center">
|
||
{% if volt.hardware %}<span class="badge bg-light text-dark border">{{ volt.hardware }}</span>{% endif %}
|
||
{% if volt.status != 'active' %}<span class="badge {% if volt.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %}">{{ volt.get_status_display }}</span>{% endif %}
|
||
</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="text-muted small p-3 mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucun hardware tension" %}</p>
|
||
{% endif %}
|
||
{% endwith %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>{# /accordion #}
|
||
|
||
{% endwith %}
|
||
{% else %}
|
||
<div class="text-center text-muted py-4">
|
||
<i class="bi bi-cpu fs-3 opacity-50 d-block mb-2"></i>
|
||
{% translate "Aucun contrôleur" %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if not intersection.is_radar %}
|
||
{# ── Onglet Programmation ── #}
|
||
<div class="tab-pane fade" id="assets-programmation" role="tabpanel" aria-labelledby="assets-programmation-tab">
|
||
|
||
{# ── Programmations ── #}
|
||
{% with prog_list=intersection.programmations.all %}
|
||
{% if prog_list %}
|
||
<div class="card border-0 shadow-sm mb-3">
|
||
<div class="card-header bg-light d-flex align-items-center gap-2 py-2">
|
||
<i class="bi bi-kanban text-secondary"></i>
|
||
<span class="fw-semibold">{% translate "Programmations" %}</span>
|
||
<span class="badge bg-secondary ms-auto">{{ prog_list|length }}</span>
|
||
</div>
|
||
<ul class="list-group list-group-flush">
|
||
{% for prog in prog_list %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2">
|
||
<i class="bi bi-kanban text-secondary small flex-shrink-0"></i>
|
||
{# Index - colonne fixe #}
|
||
<span style="width:3.5rem; flex-shrink:0;">
|
||
{% if prog.index %}<span class="badge bg-light text-dark border">{{ prog.index }}</span>{% endif %}
|
||
</span>
|
||
{# Statut - colonne fixe #}
|
||
<span style="width:7rem; flex-shrink:0;">
|
||
<span class="badge {% if prog.status == 'active' %}bg-success{% elif prog.status == 'removed' %}bg-secondary{% elif prog.status == 'damaged' or prog.status == 'to_replace' %}bg-warning text-dark{% else %}bg-info text-dark{% endif %}">{{ prog.get_status_display }}</span>
|
||
</span>
|
||
{# Code + nom #}
|
||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightprogramming' prog.id %}" class="fw-medium text-decoration-none">{{ prog.code }}</a>
|
||
{% if prog.get_name %}<span class="text-muted small">{{ prog.get_name }}</span>{% endif %}
|
||
{# Date alignée à droite #}
|
||
{% if prog.commissioning_date %}<span class="text-muted small ms-auto flex-shrink-0">{{ prog.commissioning_date|date:"d/m/Y" }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center text-muted py-4">
|
||
<i class="bi bi-kanban fs-3 opacity-50 d-block mb-2"></i>
|
||
{% translate "Aucune programmation" %}
|
||
</div>
|
||
{% endif %}
|
||
{% endwith %}
|
||
|
||
{# ── Log files du contrôleur ── #}
|
||
{% with ctrl=assets.controllers.first %}
|
||
{% if ctrl %}
|
||
{% with log_list=ctrl.logs.all|dictsortreversed:"filedate" %}
|
||
<div class="accordion" id="progLogAccordion">
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header" id="headingLogs">
|
||
<button class="accordion-button collapsed" type="button"
|
||
data-bs-toggle="collapse"
|
||
data-bs-target="#collapseLogs"
|
||
aria-expanded="false"
|
||
aria-controls="collapseLogs">
|
||
<i class="bi bi-file-earmark-text me-2 text-secondary"></i>
|
||
{% translate "Fichiers de log" %}
|
||
<span class="badge bg-secondary ms-2">{{ log_list|length }}</span>
|
||
</button>
|
||
</h2>
|
||
<div id="collapseLogs" class="accordion-collapse collapse"
|
||
aria-labelledby="headingLogs"
|
||
data-bs-parent="#progLogAccordion">
|
||
<div class="accordion-body p-0">
|
||
{% if log_list %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for log in log_list %}
|
||
<li class="list-group-item d-flex align-items-center gap-2 py-2">
|
||
<i class="bi bi-file-earmark-text text-secondary small"></i>
|
||
{% private_file_url log 'file' as log_url %}
|
||
<a href="{{ log_url }}" target="_blank" class="fw-medium text-decoration-none text-truncate" style="max-width: 60%;">{{ log.file.name|slice:"42:" }}</a>
|
||
{% if log.filedate %}<span class="text-muted small ms-auto">{{ log.filedate|date:"d/m/Y" }}</span>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="text-muted small p-3 mb-0"><i class="bi bi-info-circle me-1"></i>{% translate "Aucun fichier de log" %}</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endwith %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>{# /tab-content #}
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div class="card mb-4">
|
||
<div class="card-header" style="background: linear-gradient(to left, #e2e6ea, #f8f9fa);">
|
||
<h4>{% translate "Interventions" %}</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
<table id="interventions-table" class="table"
|
||
data-height="550"
|
||
data-pagination="true"
|
||
data-page-size="25"
|
||
data-click-to-select="true" >
|
||
<thead>
|
||
<tr>
|
||
<th data-field="actions" data-width="100"></th>
|
||
<th data-field="code">{% translate "Code" %}</th>
|
||
<th data-field="title">{% translate "Titre" %}</th>
|
||
<th data-field="begin_date">{% translate "Début" %}</th>
|
||
<th data-field="statut">{% translate "Statut" %}</th>
|
||
</tr>
|
||
<tbody>
|
||
{% for interv in interventions %}
|
||
<tr>
|
||
<td>
|
||
<a href="{% url 'interventions:interventions_index' %}{{ interv.id }}" type="button" class="btn btn-sm btn-light detail-btn" data-id="${row.id}" data-bs-toggle="tooltip" title="{% translate 'Détail' %}">
|
||
<i class="bi bi-info-circle"></i>
|
||
</a>
|
||
</td>
|
||
<td>{{ interv.code }}</td>
|
||
<td>{{ interv.title }}</td>
|
||
<td>{{ interv.begin_time|date:"d-m-Y H:i" }}</td>
|
||
<td>{{ interv.get_status_display }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</thead>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-4">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<h6 class="mb-0 font-weight-bold" style="color: #2c3e50;"><i class="bi bi-map-fill me-1"></i> {% translate "Plan" %}</h6>
|
||
</div>
|
||
<div id="map" class="shadow-sm rounded-3 border">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Asset Modal -->
|
||
{% if 'create' in allowed_location_actions %}
|
||
<div class="modal fade" id="addAssetModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">{% translate "Ajouter un asset" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<form id="addAssetForm">
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">{% translate "Catégorie" %} <span class="text-danger">*</span></label>
|
||
<select name="category_id" id="addAssetCategory" class="form-select" required>
|
||
<option value="">-- {% translate "Sélectionner une catégorie" %} --</option>
|
||
</select>
|
||
</div>
|
||
|
||
{# Type d'asset (dynamique) #}
|
||
<div class="mb-3 d-none" id="addAssetTypeContainer">
|
||
<label for="addAssetType" class="form-label">
|
||
{% translate "Type d'équipement" %} <span class="text-danger">*</span>
|
||
</label>
|
||
<select class="form-select" id="addAssetType" name="asset_type">
|
||
<option value="">{% translate "Sélectionner un type..." %}</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">{% translate "Modèle" %} <span class="text-danger" id="addAssetModelRequiredStar" style="display: none;">*</span></label>
|
||
<select name="model_id" id="addAssetModel" class="form-select" disabled>
|
||
<option value="">{% translate "Sélectionner d'abord une catégorie..." %}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-plus-circle"></i> {% translate "Créer" %}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Georeferenced Plans Selection Modal -->
|
||
<div class="modal fade" id="georefPlanModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content rounded-3 border-0 shadow-lg">
|
||
<div class="modal-header bg-light border-bottom-0 py-3">
|
||
<h5 class="modal-title font-weight-bold text-dark"><i class="bi bi-geo-alt-fill text-primary me-2"></i>{% translate "Plans géoréférencés" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body py-3">
|
||
<!-- Nav Tabs -->
|
||
<ul class="nav nav-tabs nav-fill mb-3" id="georefPlanTabs" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active font-weight-bold" id="tab-image-plan" data-bs-toggle="tab" data-bs-target="#panel-image-plan" type="button" role="tab" aria-controls="panel-image-plan" aria-selected="true">
|
||
<i class="bi bi-file-earmark-image me-1"></i>{% translate "Plan image" %}
|
||
</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link font-weight-bold" id="tab-vector-plan" data-bs-toggle="tab" data-bs-target="#panel-vector-plan" type="button" role="tab" aria-controls="panel-vector-plan" aria-selected="false">
|
||
<i class="bi bi-file-earmark-code me-1"></i>{% translate "Plan CAO" %}
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
|
||
<!-- Tab Content -->
|
||
<div class="tab-content pt-2" id="georefPlanTabContent">
|
||
<!-- Tab 1: Image Plan -->
|
||
<div class="tab-pane fade show active" id="panel-image-plan" role="tabpanel" aria-labelledby="tab-image-plan">
|
||
<div id="image-plan-selection-container">
|
||
<div id="image-plans-list" class="d-flex flex-column gap-3 mb-2">
|
||
<!-- Dynamically populated list of images with switches and sliders -->
|
||
</div>
|
||
<div id="no-georef-images-alert" class="text-center py-4" style="display: none;">
|
||
<div class="text-muted mb-3"><i class="bi bi-info-circle" style="font-size: 2.5rem;"></i></div>
|
||
<p class="text-dark font-weight-semibold mb-1">{% translate "Aucun plan d'image géoréférencé disponible" %}</p>
|
||
<p class="text-muted small">{% translate "Géoréférencez d'abord un plan PDF ou image dans l'onglet Documents de la fiche." %}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab 2: Vector Plan (DXF) -->
|
||
<div class="tab-pane fade" id="panel-vector-plan" role="tabpanel" aria-labelledby="tab-vector-plan">
|
||
<div id="dxf-selection-container">
|
||
{% if not dxf_files %}
|
||
<div class="text-center py-4">
|
||
<div class="text-muted mb-3"><i class="bi bi-exclamation-triangle" style="font-size: 2.5rem;"></i></div>
|
||
<p class="text-dark font-weight-semibold mb-1">{% translate "Aucun fichier DXF disponible pour ce carrefour" %}</p>
|
||
<p class="text-muted small">{% translate "Veuillez d'abord en importer un." %}</p>
|
||
<a href="{% url 'assets:import_dxf' intersection.id %}" class="btn btn-primary rounded-3 btn-sm mt-2 px-3">
|
||
<i class="bi bi-upload me-1"></i>{% translate "Importer un plan DXF" %}
|
||
</a>
|
||
</div>
|
||
{% else %}
|
||
<!-- File Selection -->
|
||
<div class="mb-3">
|
||
<label class="form-label font-weight-semibold text-muted small uppercase">{% translate "Sélectionner le plan DXF" %}</label>
|
||
{% if dxf_files|length == 1 %}
|
||
<div class="p-3 bg-light border rounded-3 font-weight-semibold text-dark mb-3">
|
||
<i class="bi bi-file-earmark-check-fill text-success me-2"></i>{{ dxf_files.0 }}
|
||
<input type="hidden" id="select-dxf-file" value="{{ dxf_files.0 }}">
|
||
</div>
|
||
{% else %}
|
||
<select id="select-dxf-file" class="form-select rounded-3 p-3 mb-3">
|
||
<option value="">{% translate "-- Sélectionner un plan CAO (DXF) --" %}</option>
|
||
{% for f in dxf_files %}
|
||
<option value="{{ f }}">{{ f }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Layers selection checklist -->
|
||
<div id="dxf-detail-layers-section" style="display: none;" class="mb-3">
|
||
<label class="form-label font-weight-semibold text-muted small uppercase d-flex justify-content-between align-items-center mb-2">
|
||
<span>{% translate "Couches vectorielles CAO" %}</span>
|
||
<button type="button" id="dxf-detail-toggle-all-layers" class="btn btn-link btn-sm p-0 text-decoration-none" style="font-size: 0.75rem;">
|
||
{% translate "Décocher tout" %}
|
||
</button>
|
||
</label>
|
||
<div id="dxf-detail-layers-spinner" class="text-center py-4" style="display: none;">
|
||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||
<div class="text-muted small mt-2">{% translate "Lecture des couches du plan..." %}</div>
|
||
</div>
|
||
<div id="dxf-detail-layers-list" class="border rounded-3 p-3 bg-white" style="max-height: 250px; overflow-y: auto;">
|
||
<!-- Checkboxes will be populated dynamically -->
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer border-top-0 pt-0 pb-4 justify-content-between px-4">
|
||
<button type="button" id="btn-disable-all-plans" class="btn btn-outline-danger px-3 rounded-3">
|
||
<i class="bi bi-eye-slash-fill me-1"></i>{% translate "Désactiver tout" %}
|
||
</button>
|
||
<button type="button" class="btn btn-secondary px-4 rounded-3 font-weight-semibold shadow-sm" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock content%}
|
||
|
||
|
||
{% block script %}
|
||
{{ intersection.lon|json_script:"lon_value" }}
|
||
{{ intersection.lat|json_script:"lat_value" }}
|
||
{{ intersection.status|json_script:"status_value" }}
|
||
{{ intersection.is_radar|json_script:"is_radar_value" }}
|
||
{{ assets_geojson|json_script:"assets_geojson" }}
|
||
<script>
|
||
const trafficlightintersectionsurl = "{% url 'assets:trafficlights_intersections' %}";
|
||
const document_history_url = "{% url 'assets:trafficlights_intersections_document_history' %}";
|
||
const feature_lon = JSON.parse(document.getElementById("lon_value").textContent);
|
||
const feature_lat = JSON.parse(document.getElementById("lat_value").textContent);
|
||
const feature_status = JSON.parse(document.getElementById("status_value").textContent);
|
||
const feature_is_radar = JSON.parse(document.getElementById("is_radar_value").textContent);
|
||
const assets_geojson = JSON.parse(document.getElementById("assets_geojson").textContent);
|
||
const traffic_light_icon_url = "{% static 'assets/traffic_light_icon.svg' %}";
|
||
const simple_pole_icon_url = "{% static 'assets/simple_pole_icon.svg' %}";
|
||
const electrical_cabinet_icon_url = "{% static 'assets/electrical-panel.png' %}";
|
||
const radar_preventif_icon_url = "{% static 'assets/radar_preventif_icon.svg' %}?v=20260710";
|
||
const thematicCode = "{{ thematic_code }}";
|
||
const intersectionId = "{{ intersection.id }}";
|
||
const dxfFiles = {% if dxf_files %}{{ dxf_files|safe }}{% else %}[]{% endif %};
|
||
</script>
|
||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
||
<script src="{% static 'assets/trafficlights_intersection_detail.js' %}"></script>
|
||
{% if allowed_location_actions %}
|
||
<script src="{% static 'assets/asset_cms_edit_mode.js' %}"></script>
|
||
{% endif %}
|
||
|
||
{# ── Location editing modals (edit / delete) ── #}
|
||
{% if can_edit_location %}
|
||
{% include "assets/components/location_edit_modals.html" with thematic_code=thematic_code location_type=location_type entry_label="Intersection" %}
|
||
|
||
{# ── Contracts management modal ── #}
|
||
<div class="modal fade" id="contractsEditModal" tabindex="-1" aria-labelledby="contractsEditModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="contractsEditModalLabel">
|
||
<i class="bi bi-file-earmark-text me-2"></i>{% translate "Gérer les contrats" %}
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
{# Current contracts list #}
|
||
<h6 class="mb-3">{% translate "Contrats associés" %}</h6>
|
||
<div id="contractsList">
|
||
{% if trafficlight_contracts %}
|
||
<table class="table table-sm table-bordered">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>{% translate "Numéro de contrat" %}</th>
|
||
<th>{% translate "Entreprise" %}</th>
|
||
<th>{% translate "Type de maintenance" %}</th>
|
||
<th>{% translate "Statut" %}</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="contractsTableBody">
|
||
{% for tlc in trafficlight_contracts %}
|
||
<tr id="contract-row-{{ tlc.pk }}">
|
||
<td>{{ tlc.contract.contract_number }}</td>
|
||
<td>{{ tlc.contract.company.name }}</td>
|
||
<td>{{ tlc.get_maintenance_type_display }}</td>
|
||
<td>{{ tlc.get_status_display }}</td>
|
||
<td>
|
||
<button type="button" class="btn btn-sm btn-outline-danger contract-remove-btn"
|
||
data-tl-contract-id="{{ tlc.pk }}"
|
||
data-url="{% url 'assets:trafficlights_intersections_contracts_remove' intersection.id tlc.pk %}"
|
||
title="{% translate 'Retirer ce contrat' %}">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p class="text-muted fst-italic" id="contractsEmptyMsg">{% translate "Aucun contrat associé." %}</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
{# Add contract form #}
|
||
<h6 class="mb-3">{% translate "Ajouter un contrat" %}</h6>
|
||
<form id="addContractForm">
|
||
{% csrf_token %}
|
||
<div class="row g-2 align-items-end">
|
||
<div class="col-md-5">
|
||
<label class="form-label">{% translate "Contrat" %}</label>
|
||
<select id="contractSelect" name="contract_id" class="form-select form-select-sm" required>
|
||
<option value="">— {% translate "Sélectionner" %} —</option>
|
||
{% for contract in available_contracts %}
|
||
<option value="{{ contract.pk }}">{{ contract.contract_number }} — {{ contract.company.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">{% translate "Type" %}</label>
|
||
<select id="maintenanceTypeSelect" name="maintenance_type" class="form-select form-select-sm">
|
||
<option value="infra">{% translate "Infrastructure" %}</option>
|
||
<option value="controller">{% translate "Contrôleur de traffic" %}</option>
|
||
<option value="other" selected>{% translate "Autre" %}</option>
|
||
</select>
|
||
</div>
|
||
<input type="hidden" name="status" value="active">
|
||
<div class="col-md-2">
|
||
<button type="submit" class="btn btn-sm btn-primary w-100">
|
||
<i class="bi bi-plus-circle me-1"></i>{% translate "Ajouter" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="addContractAlert" class="alert d-none mt-2" role="alert"></div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
const addUrl = "{% url 'assets:trafficlights_intersections_contracts_add' intersection.id %}";
|
||
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || '{{ csrf_token }}';
|
||
|
||
function getTableBody() {
|
||
return document.getElementById('contractsTableBody');
|
||
}
|
||
|
||
function ensureTable() {
|
||
// If there was an empty message, replace it with a table
|
||
let emptyMsg = document.getElementById('contractsEmptyMsg');
|
||
if (emptyMsg) {
|
||
emptyMsg.remove();
|
||
}
|
||
let existing = document.getElementById('contractsTableBody');
|
||
if (existing) return existing;
|
||
const listDiv = document.getElementById('contractsList');
|
||
listDiv.innerHTML = `<table class="table table-sm table-bordered">
|
||
<thead class="table-light"><tr>
|
||
<th>{% translate "Numéro de contrat" %}</th>
|
||
<th>{% translate "Entreprise" %}</th>
|
||
<th>{% translate "Type de maintenance" %}</th>
|
||
<th>{% translate "Statut" %}</th>
|
||
<th></th>
|
||
</tr></thead>
|
||
<tbody id="contractsTableBody"></tbody>
|
||
</table>`;
|
||
return document.getElementById('contractsTableBody');
|
||
}
|
||
|
||
function bindRemoveButtons() {
|
||
document.querySelectorAll('.contract-remove-btn').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
const url = this.dataset.url;
|
||
const row = document.getElementById('contract-row-' + this.dataset.tlContractId);
|
||
fetch(url, {
|
||
method: 'POST',
|
||
headers: {'X-CSRFToken': csrfToken, 'X-Requested-With': 'XMLHttpRequest'},
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.success && row) {
|
||
row.remove();
|
||
// Refresh badges in header
|
||
refreshContractBadges();
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function refreshContractBadges() {
|
||
const badgesContainer = document.querySelector('.contract-badges-container');
|
||
if (!badgesContainer) return;
|
||
const rows = document.querySelectorAll('#contractsTableBody tr');
|
||
badgesContainer.innerHTML = '';
|
||
if (rows.length === 0) {
|
||
badgesContainer.innerHTML = '<span class="text-muted fst-italic">{% translate "Aucun" %}</span>';
|
||
return;
|
||
}
|
||
rows.forEach(function(row) {
|
||
const cells = row.querySelectorAll('td');
|
||
const num = cells[0]?.textContent.trim();
|
||
const type = cells[2]?.textContent.trim();
|
||
const badge = document.createElement('span');
|
||
badge.className = 'badge bg-secondary me-1';
|
||
badge.title = type;
|
||
badge.innerHTML = num + ' <span class="text-light opacity-75 ms-1">(' + type + ')</span>';
|
||
badgesContainer.appendChild(badge);
|
||
});
|
||
}
|
||
|
||
document.getElementById('addContractForm').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
const alertEl = document.getElementById('addContractAlert');
|
||
alertEl.className = 'alert d-none mt-2';
|
||
|
||
const formData = new FormData(this);
|
||
fetch(addUrl, {
|
||
method: 'POST',
|
||
headers: {'X-CSRFToken': csrfToken, 'X-Requested-With': 'XMLHttpRequest'},
|
||
body: formData,
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const tbody = ensureTable();
|
||
const removeUrl = "{% url 'assets:trafficlights_intersections_contracts_remove' intersection.id 0 %}".replace('/0/', '/' + data.id + '/');
|
||
const tr = document.createElement('tr');
|
||
tr.id = 'contract-row-' + data.id;
|
||
tr.innerHTML = `<td>${data.contract_number}</td>
|
||
<td>${data.company}</td>
|
||
<td>${data.maintenance_type}</td>
|
||
<td>${data.status}</td>
|
||
<td><button type="button" class="btn btn-sm btn-outline-danger contract-remove-btn"
|
||
data-tl-contract-id="${data.id}"
|
||
data-url="${removeUrl}"
|
||
title="{% translate 'Retirer ce contrat' %}">
|
||
<i class="bi bi-trash"></i></button></td>`;
|
||
tbody.appendChild(tr);
|
||
bindRemoveButtons();
|
||
refreshContractBadges();
|
||
document.getElementById('contractSelect').value = '';
|
||
} else {
|
||
alertEl.textContent = data.error || "{% translate 'Erreur lors de l’ajout.' %}";
|
||
alertEl.className = 'alert alert-danger mt-2';
|
||
}
|
||
});
|
||
});
|
||
|
||
bindRemoveButtons();
|
||
})();
|
||
</script>
|
||
{% endif %}
|
||
|
||
<script>
|
||
// Expand / collapse all pole accordions
|
||
(function () {
|
||
const btn = document.getElementById('expandAllPoles');
|
||
if (!btn) return;
|
||
let expanded = false;
|
||
btn.addEventListener('click', function () {
|
||
expanded = !expanded;
|
||
document.querySelectorAll('#accordionPoles .collapse').forEach(function (el) {
|
||
const bsCollapse = bootstrap.Collapse.getOrCreateInstance(el, { toggle: false });
|
||
expanded ? bsCollapse.show() : bsCollapse.hide();
|
||
});
|
||
btn.querySelector('i').className = expanded
|
||
? 'bi bi-chevron-contract small'
|
||
: 'bi bi-chevron-expand small';
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock script %}
|