1876 lines
No EOL
94 KiB
HTML
1876 lines
No EOL
94 KiB
HTML
{% extends base_template|default:"base.html" %}
|
||
{% load i18n %}
|
||
{% load static %}
|
||
{% load asset_tags %}
|
||
|
||
{% block head %}
|
||
<!-- MapLibre GL 5.7.0 -->
|
||
<link href="{% static 'common/maplibre-gl.css' %}" rel="stylesheet">
|
||
<link rel="stylesheet" href="{% static 'assets/asset_cms_edit_mode.css' %}">
|
||
{% if asset.geom or asset.lon or asset.lat %}
|
||
<!-- MapboxDraw for geometry editing -->
|
||
<link href="{% static 'common/mapbox-gl-draw.css' %}" rel="stylesheet"/>
|
||
{% endif %}
|
||
<style>
|
||
.replacement-chain {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.replacement-item {
|
||
position: relative;
|
||
}
|
||
|
||
.replacement-badge {
|
||
display: inline-block;
|
||
padding: 0.5rem 0.75rem;
|
||
background-color: #e9ecef;
|
||
border: 2px solid #6c757d;
|
||
border-radius: 0.5rem;
|
||
font-size: 0.95rem;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.replacement-badge:hover {
|
||
background-color: #dee2e6;
|
||
border-color: #495057;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.replacement-badge.current {
|
||
background-color: #cfe2ff;
|
||
border-color: #0d6efd;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.replacement-arrow {
|
||
font-size: 1.25rem;
|
||
color: #6c757d;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Fix for mobile: force decimal separator to dot for coordinate inputs */
|
||
input[type="number"][data-coordinate] {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* Accentuation des bordures de cartes pour meilleure distinction */
|
||
#asset-detail-container .card {
|
||
border: 1px solid #b8c5d3;
|
||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
#asset-detail-container .card-header {
|
||
border-bottom: 1px solid #b8c5d3;
|
||
background-color: #f8fafc;
|
||
}
|
||
|
||
/* Adoucissement des lignes alternées des tables (stripes très clairs et élégants) */
|
||
#asset-detail-container .table-striped {
|
||
--bs-table-striped-bg: #f8fafc;
|
||
--bs-table-accent-bg: #f8fafc;
|
||
}
|
||
#asset-detail-container .table-striped > tbody > tr:nth-of-type(odd) > * {
|
||
--bs-table-bg-type: #f8fafc;
|
||
--bs-table-accent-bg: #f8fafc;
|
||
background-color: #f8fafc !important;
|
||
box-shadow: inset 0 0 0 9999px #f8fafc !important;
|
||
}
|
||
#asset-detail-container .table-striped > tbody > tr:nth-of-type(even) > * {
|
||
--bs-table-bg-type: #ffffff;
|
||
--bs-table-accent-bg: #ffffff;
|
||
background-color: #ffffff !important;
|
||
box-shadow: inset 0 0 0 9999px #ffffff !important;
|
||
}
|
||
#asset-detail-container .table th {
|
||
color: #475569;
|
||
font-weight: 500;
|
||
border-color: #e2e8f0;
|
||
}
|
||
#asset-detail-container .table td {
|
||
border-color: #e2e8f0;
|
||
}
|
||
|
||
/* Styles pour la barre d'édition et mode édition mobile */
|
||
#asset-cms-edit-bar.mobile-edit-bar {
|
||
position: sticky;
|
||
top: 4.5rem;
|
||
z-index: 1020;
|
||
background-color: #ffffff;
|
||
border: 1px solid #b8c5d3;
|
||
box-shadow: 0 4px 6px -1px rgba(15, 23, 42, 0.08);
|
||
}
|
||
#asset-cms-edit-bar .mode-editing-label {
|
||
display: none;
|
||
}
|
||
#asset-cms-edit-bar .mode-viewing-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
}
|
||
body.asset-cms-edit-mode #asset-cms-edit-bar .mode-editing-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
}
|
||
body.asset-cms-edit-mode #asset-cms-edit-bar .mode-viewing-label {
|
||
display: none;
|
||
}
|
||
body.asset-cms-edit-mode #asset-edit-mode-badge {
|
||
background-color: #fef08a !important;
|
||
color: #854d0e !important;
|
||
border-color: #fde047 !important;
|
||
}
|
||
|
||
@media (max-width: 767.98px) {
|
||
.asset-editable-field .field-edit input,
|
||
.asset-editable-field .field-edit select,
|
||
.asset-editable-field .field-edit textarea {
|
||
min-height: 42px;
|
||
font-size: 1rem;
|
||
}
|
||
#asset-map {
|
||
height: 280px !important;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock head %}
|
||
|
||
{% block extra_head %}
|
||
<!-- MapLibre GL 5.7.0 -->
|
||
<link href="{% static 'common/maplibre-gl.css' %}" rel="stylesheet">
|
||
<link rel="stylesheet" href="{% static 'assets/asset_cms_edit_mode.css' %}">
|
||
{% if asset.geom or asset.lon or asset.lat %}
|
||
<!-- MapboxDraw for geometry editing -->
|
||
<link href="{% static 'common/mapbox-gl-draw.css' %}" rel="stylesheet"/>
|
||
{% endif %}
|
||
{% endblock extra_head %}
|
||
|
||
{% block upleft %}
|
||
{% if mobile_view and back_url %}
|
||
<a href="{{ back_url }}" class="nav-link d-flex align-items-center gap-1" style="color: white;" title="{% translate 'Retour' %}">
|
||
<i class="bi bi-arrow-left fs-4"></i>
|
||
</a>
|
||
{% elif mobile_view or is_mobile %}
|
||
<a href="{% url 'mobile:mobile_index' %}?home=1" class="nav-link" style="color: white;" title="{% translate 'Accueil' %}">
|
||
<i class="bi bi-house fs-3"></i>
|
||
</a>
|
||
{% else %}
|
||
<a href="/" class="nav-link" style="color: white;">
|
||
<i class="bi bi-house fs-3" title="Home"></i>
|
||
</a>
|
||
{% endif %}
|
||
{% endblock upleft %}
|
||
|
||
{% block title %}
|
||
{% if mobile_view %}
|
||
<span>{{ asset.code }} - {% translate "Détails" %}</span>
|
||
{% else %}
|
||
{{ product_name }}
|
||
{% endif %}
|
||
{% endblock title %}
|
||
|
||
{% block upright %}
|
||
<a href="{% url 'common:account_info' %}?next={{ request.get_full_path|urlencode }}" class="nav-link" style="color: white;">
|
||
<i class="bi bi-person-gear fs-3" title="Account"></i>
|
||
</a>
|
||
{% endblock upright %}
|
||
|
||
{% block content %}
|
||
<div id="asset-detail-container"
|
||
class="{% if mobile_view %}px-2 px-sm-3 pb-3{% else %}container-fluid mt-4{% endif %}"
|
||
data-asset-model="{{ asset_model }}"
|
||
data-asset-id="{{ asset.id }}"
|
||
data-thematic-code="{{ thematic_code }}"
|
||
data-has-support="{% if asset.pole_id or asset.publiclighting_pole_id or asset.trafficlights_pole_id %}true{% else %}false{% endif %}"
|
||
{% if asset_geometry_json %}data-asset-geometry="{{ asset_geometry_json }}"{% endif %}
|
||
{% if thematic_code == 'trafficlights' %}
|
||
data-tl-icon-url="{% static 'assets/traffic_light_icon.svg' %}"
|
||
data-simple-pole-icon-url="{% static 'assets/simple_pole_icon.svg' %}"
|
||
data-cabinet-icon-url="{% static 'assets/electrical-panel.png' %}"
|
||
data-radar-icon-url="{% static 'assets/radar_preventif_icon.svg' %}?v=20260710"
|
||
{% endif %}>
|
||
|
||
<!-- CMS-style Edit Bar (Desktop & Mobile) -->
|
||
{% if mobile_view %}
|
||
{% if allowed_update_fields %}
|
||
<div id="asset-cms-edit-bar" class="mobile-edit-bar rounded-3 p-2 mb-3 d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<span id="asset-edit-mode-badge" class="badge bg-secondary-subtle text-secondary border">
|
||
<span class="mode-viewing-label"><i class="bi bi-eye me-1"></i>{% translate "Consultation" %}</span>
|
||
<span class="mode-editing-label"><i class="bi bi-pencil-fill me-1"></i>{% translate "Édition en cours" %}</span>
|
||
</span>
|
||
<span class="fw-bold small text-truncate" style="max-width: 140px;" title="{{ asset.code }}">
|
||
{{ asset.code }}
|
||
</span>
|
||
</div>
|
||
<div class="d-flex align-items-center gap-1 ms-auto">
|
||
{% if 'archive' in allowed_actions %}
|
||
<button id="asset-archive-btn" class="btn btn-outline-secondary btn-sm asset-normal-action px-2" title="{% translate 'Archiver' %}">
|
||
<i class="bi bi-archive"></i>
|
||
</button>
|
||
{% endif %}
|
||
{% if 'replace' in allowed_actions %}
|
||
<button id="asset-replace-btn" class="btn btn-outline-secondary btn-sm asset-normal-action px-2" title="{% translate 'Remplacer' %}">
|
||
<i class="bi bi-arrow-repeat"></i>
|
||
</button>
|
||
{% endif %}
|
||
<button id="asset-cms-edit-btn" class="btn btn-primary btn-sm px-3 fw-semibold">
|
||
<i class="bi bi-pencil me-1"></i>{% translate "Éditer" %}
|
||
</button>
|
||
<button id="asset-cms-save-btn" class="btn btn-success btn-sm px-3 fw-semibold d-none">
|
||
<i class="bi bi-check-circle me-1"></i>{% translate "Sauvegarder" %}
|
||
</button>
|
||
<button id="asset-cms-cancel-btn" class="btn btn-outline-danger btn-sm px-2 d-none">
|
||
<i class="bi bi-x-circle me-1"></i>{% translate "Annuler" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
{% if allowed_update_fields %}
|
||
<div id="asset-cms-edit-bar" class="px-3 py-2 mb-3 shadow-sm d-flex align-items-center justify-content-between border-bottom bg-light">
|
||
<div class="d-flex align-items-center gap-2">
|
||
{% block asset_header_title %}
|
||
<div>
|
||
<h2 class="mb-0" style="color: #0f172a; font-size: 1.25rem; font-weight: 700;">
|
||
{% translate "Asset" %} : {{ asset.code }}
|
||
</h2>
|
||
<div class="d-flex align-items-center gap-2 mt-1" style="font-size: 0.8rem; color: #6c757d;">
|
||
{% if thematic.icon %}
|
||
<i class="bi {{ thematic.icon }}"></i>
|
||
{% endif %}
|
||
{% if thematic.name %}
|
||
<span>{{ thematic.name }}</span>
|
||
<span>—</span>
|
||
{% endif %}
|
||
<span>{{ asset_verbose_name }}</span>
|
||
</div>
|
||
</div>
|
||
{% endblock asset_header_title %}
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
{% if 'archive' in allowed_actions %}
|
||
<button id="asset-archive-btn" class="btn btn-light btn-sm asset-normal-action" title="{% translate 'Archiver' %}">
|
||
<i class="bi bi-archive"></i> {% translate "Archiver" %}
|
||
</button>
|
||
{% endif %}
|
||
{% if 'replace' in allowed_actions %}
|
||
<button id="asset-replace-btn" class="btn btn-light btn-sm asset-normal-action" title="{% translate 'Remplacer' %}">
|
||
<i class="bi bi-arrow-repeat"></i> {% translate "Remplacer" %}
|
||
</button>
|
||
{% endif %}
|
||
<button id="asset-cms-edit-btn" class="btn btn-light btn-sm">
|
||
<i class="bi bi-pencil"></i> {% translate "Éditer" %}
|
||
</button>
|
||
<button id="asset-cms-save-btn" class="btn btn-success btn-sm d-none">
|
||
<i class="bi bi-check-circle"></i> {% translate "Sauvegarder" %}
|
||
</button>
|
||
<button id="asset-cms-cancel-btn" class="btn btn-outline-danger btn-sm d-none">
|
||
<i class="bi bi-x-circle"></i> {% translate "Annuler" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% endif %}
|
||
|
||
{% if mobile_view or not allowed_update_fields %}
|
||
<div class="mb-3">
|
||
{% block asset_header_title_readonly %}
|
||
<h2 class="mb-0" style="color: #0f172a; font-size: 1.25rem; font-weight: 700;">
|
||
{% if thematic.icon %}
|
||
<i class="bi {{ thematic.icon }} me-2"></i>
|
||
{% endif %}
|
||
{% translate "Asset" %} : {{ asset.code }}
|
||
</h2>
|
||
<div class="d-flex align-items-center gap-2 mt-1" style="font-size: 0.85rem; color: #6c757d;">
|
||
{% if thematic.icon %}
|
||
<i class="bi {{ thematic.icon }}"></i>
|
||
{% endif %}
|
||
{% if thematic.name %}
|
||
<span>{{ thematic.name }}</span>
|
||
<span>—</span>
|
||
{% endif %}
|
||
<span>{{ asset_verbose_name }}</span>
|
||
</div>
|
||
{% endblock asset_header_title_readonly %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="row {% if not mobile_view %}mt-4{% endif %}">
|
||
<!-- Colonne principale : informations de base -->
|
||
<div class="{% if mobile_view %}col-12{% else %}col-md-8{% endif %}">
|
||
{% block main_asset_content %}
|
||
<div class="card mb-3">
|
||
<div class="card-header bg-light">
|
||
<h5 class="mb-0">{% translate "Informations générales" %}</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<table class="table table-striped mb-0">
|
||
<tbody>
|
||
<!-- Code -->
|
||
<tr class="asset-editable-field" data-field="code" data-can-edit="{% if 'code' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th style="width: 30%;">{% translate "Code" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.code }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="text" class="form-control form-control-sm" value="{{ asset.code }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Référence externe -->
|
||
{% if asset.external_reference or 'external_reference' in allowed_update_fields %}
|
||
<tr class="asset-editable-field" data-field="external_reference" data-can-edit="{% if 'external_reference' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Référence externe" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.external_reference|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="text" class="form-control form-control-sm" value="{{ asset.external_reference|default:'' }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
|
||
<!-- Nom FR -->
|
||
<tr class="asset-editable-field" data-field="name_fr" data-can-edit="{% if 'name_fr' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Nom" %} (FR)</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.name_fr|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="text" class="form-control form-control-sm" value="{{ asset.name_fr|default:'' }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Nom NL -->
|
||
<tr class="asset-editable-field" data-field="name_nl" data-can-edit="{% if 'name_nl' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Nom" %} (NL)</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.name_nl|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="text" class="form-control form-control-sm" value="{{ asset.name_nl|default:'' }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Statut -->
|
||
<tr class="asset-editable-field" data-field="status" data-can-edit="{% if 'status' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Statut" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">
|
||
{% if asset.status == 'active' %}
|
||
<span class="badge bg-success">{{ asset.get_status_display }}</span>
|
||
{% elif asset.status == 'damaged' %}
|
||
<span class="badge bg-warning">{{ asset.get_status_display }}</span>
|
||
{% elif asset.status == 'to_replace' %}
|
||
<span class="badge bg-danger">{{ asset.get_status_display }}</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary">{{ asset.get_status_display }}</span>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<select class="form-select form-select-sm">
|
||
{% for value, label in status_choices %}
|
||
<option value="{{ value }}" {% if value == asset.status %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</tr>
|
||
|
||
<!-- Statut de validation -->
|
||
{% if asset.validation_status is not None or 'validation_status' in allowed_update_fields %}
|
||
<tr class="asset-editable-field" data-field="validation_status" data-can-edit="{% if 'validation_status' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Statut de validation" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">
|
||
{% if asset.validation_status == 'validated' %}
|
||
<span class="badge bg-success">{{ asset.get_validation_status_display }}</span>
|
||
{% elif asset.validation_status == 'to_be_validated' %}
|
||
<span class="badge bg-warning text-dark">{{ asset.get_validation_status_display }}</span>
|
||
{% elif asset.validation_status == 'to_be_completed' %}
|
||
<span class="badge bg-info text-dark">{{ asset.get_validation_status_display }}</span>
|
||
{% elif asset.validation_status == 'rejected' %}
|
||
<span class="badge bg-danger">{{ asset.get_validation_status_display }}</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary">{{ asset.get_validation_status_display|default:"-" }}</span>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<select class="form-select form-select-sm">
|
||
<option value="">-</option>
|
||
{% for value, label in validation_status_choices %}
|
||
<option value="{{ value }}" {% if value == asset.validation_status %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
|
||
<!-- Catégorie -->
|
||
<tr class="asset-editable-field" data-field="category" data-can-edit="{% if 'category' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Catégorie" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.category|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<select class="form-select form-select-sm">
|
||
<option value="">-</option>
|
||
{% for cat in categories %}
|
||
<option value="{{ cat.id }}" {% if cat.id == asset.category_id %}selected{% endif %}>{{ cat.get_name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Date d'installation -->
|
||
<tr class="asset-editable-field" data-field="installation_date" data-can-edit="{% if 'installation_date' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Installé le" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.installation_date|date:"d/m/Y"|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="date" class="form-control form-control-sm" value="{{ asset.installation_date|date:'Y-m-d'|default:'' }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- N° de série -->
|
||
<tr class="asset-editable-field" data-field="serial_number" data-can-edit="{% if 'serial_number' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "N° de série" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.serial_number|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="text" class="form-control form-control-sm" value="{{ asset.serial_number|default:'' }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Marque -->
|
||
<tr class="asset-editable-field" data-field="brand" data-can-edit="{% if 'brand' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Marque" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.brand|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="text" class="form-control form-control-sm" value="{{ asset.brand|default:'' }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Programme de financement / Projet -->
|
||
{% if asset.funding_program or 'funding_program' in allowed_update_fields %}
|
||
<tr class="asset-editable-field" data-field="funding_program" data-can-edit="{% if 'funding_program' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Programme / Financement" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.funding_program|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="text" class="form-control form-control-sm" value="{{ asset.funding_program|default:'' }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
|
||
<!-- Durée de garantie -->
|
||
<tr class="asset-editable-field" data-field="warranty_duration" data-can-edit="{% if 'warranty_duration' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Durée de garantie" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">
|
||
{% if asset.warranty_duration %}
|
||
{{ asset.warranty_duration }} {% translate "mois" %}
|
||
{% with remaining=asset|warranty_remaining_months %}
|
||
{% if remaining is not None %}
|
||
—
|
||
{% if remaining > 0 %}
|
||
<span class="badge bg-success ms-1">{% blocktranslate with n=remaining %}{{ n }} mois restants{% endblocktranslate %}</span>
|
||
{% elif remaining == 0 %}
|
||
<span class="badge bg-warning text-dark ms-1">{% translate "Expire ce mois-ci" %}</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary ms-1">{% blocktranslate with n=remaining|cut:"-" %}Expirée ({{ n }} mois){% endblocktranslate %}</span>
|
||
{% endif %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
{% else %}-{% endif %}
|
||
</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="number" class="form-control form-control-sm" value="{{ asset.warranty_duration|default:'' }}" min="0">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Dernière inspection -->
|
||
<tr class="asset-editable-field" data-field="last_inspection_date" data-can-edit="{% if 'last_inspection_date' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Dernière inspection" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ asset.last_inspection_date|date:"d/m/Y"|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="date" class="form-control form-control-sm" value="{{ asset.last_inspection_date|date:'Y-m-d'|default:'' }}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Note générale -->
|
||
<tr class="asset-editable-field" data-field="note" data-can-edit="{% if 'note' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{% translate "Note générale" %}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value text-muted">{{ asset.note|default:"-"|linebreaksbr }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<textarea class="form-control form-control-sm" rows="3">{{ asset.note|default:'' }}</textarea>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<!-- Champs spécifiques au type d'asset -->
|
||
{% get_asset_specific_fields asset as asset_specific_fields %}
|
||
{% if asset_specific_fields %}
|
||
<tr style="--bs-table-bg-type: transparent; --bs-table-bg-state: transparent;">
|
||
<td colspan="2" style="padding-top: 1.25rem; padding-bottom: 0;"></td>
|
||
</tr>
|
||
<!-- <tr>
|
||
<td colspan="2" class="p-0">
|
||
<hr class="my-0" style="border-top: 2px solid #dee2e6; ">
|
||
</td>
|
||
</tr> -->
|
||
<tr class="table-light" style="--bs-table-bg-type: transparent; --bs-table-bg-state: transparent;">
|
||
<th colspan="2" class="text-muted" style="color: #475569 !important; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em;">
|
||
{% translate "Attributs spécifiques" %}
|
||
</th>
|
||
</tr>
|
||
{% endif %}
|
||
{% for field in asset_specific_fields %}
|
||
<tr class="asset-editable-field" data-field="{{ field.name }}" data-can-edit="{% if field.name in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<th>{{ field.label }}</th>
|
||
<td>
|
||
<div class="field-display">
|
||
<span class="field-value">{{ field.display_value }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
{% if field.field_type == 'boolean' %}
|
||
<select class="form-select form-select-sm">
|
||
<option value="" {% if field.input_value == '' %}selected{% endif %}>---------</option>
|
||
<option value="true" {% if field.input_value == 'true' %}selected{% endif %}>{% translate "Oui" %}</option>
|
||
<option value="false" {% if field.input_value == 'false' %}selected{% endif %}>{% translate "Non" %}</option>
|
||
</select>
|
||
{% elif field.field_type == 'select' %}
|
||
<select class="form-select form-select-sm">
|
||
<option value="">---------</option>
|
||
{% for choice_val, choice_label in field.choices %}
|
||
<option value="{{ choice_val }}" {% if field.input_value == choice_val %}selected{% endif %}>{{ choice_label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
{% elif field.field_type == 'number' %}
|
||
<input type="number" step="any" class="form-control form-control-sm" value="{{ field.input_value }}">
|
||
{% elif field.field_type == 'date' %}
|
||
<input type="date" class="form-control form-control-sm" value="{{ field.input_value }}">
|
||
{% elif field.field_type == 'textarea' %}
|
||
<textarea class="form-control form-control-sm" rows="3">{{ field.input_value }}</textarea>
|
||
{% else %}
|
||
<input type="text" class="form-control form-control-sm" value="{{ field.input_value }}">
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endblock main_asset_content %}
|
||
|
||
{% block asset_documents_block %}
|
||
<!-- Documents associés -->
|
||
{% asset_documents asset %}
|
||
{% endblock asset_documents_block %}
|
||
|
||
<!-- Blocs supplémentaires (extensible par les templates spécifiques) -->
|
||
{% block extra_blocks %}{% endblock extra_blocks %}
|
||
|
||
<!-- Panneau de gestion des permissions (admins uniquement) -->
|
||
{% if is_admin and content_type %}
|
||
<div class="card mb-3">
|
||
<div class="card-header bg-light">
|
||
<div class="d-flex align-items-center justify-content-between" role="button"
|
||
data-bs-toggle="collapse" data-bs-target="#permissions-panel-collapse"
|
||
aria-expanded="false" aria-controls="permissions-panel-collapse"
|
||
style="cursor:pointer;"
|
||
id="permissions-panel-header">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-shield-lock me-2"></i>{% translate "Permissions spécifiques" %}
|
||
</h5>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<a href="{% url 'assets:asset_type_permissions_page' content_type.pk %}"
|
||
class="btn btn-outline-secondary btn-sm"
|
||
title="{% translate 'Voir tous les accès pour ce type' %}"
|
||
onclick="event.stopPropagation();">
|
||
<i class="bi bi-list-ul me-1"></i>{% translate "Vue par type" %}
|
||
</a>
|
||
<i class="bi bi-chevron-down"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="collapse" id="permissions-panel-collapse">
|
||
<div class="card-body p-0" id="permissions-panel-body"
|
||
data-load-url="{% url 'assets:asset_permissions_panel' content_type.pk asset.pk %}">
|
||
<div class="p-3 text-muted" id="permissions-panel-loading">
|
||
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||
{% translate "Chargement des permissions…" %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function(){
|
||
var loaded = false;
|
||
var collapse = document.getElementById('permissions-panel-collapse');
|
||
if (!collapse) return;
|
||
collapse.addEventListener('show.bs.collapse', function(){
|
||
if (loaded) return;
|
||
loaded = true;
|
||
var body = document.getElementById('permissions-panel-body');
|
||
var url = body.getAttribute('data-load-url');
|
||
fetch(url, {headers: {'X-Requested-With': 'XMLHttpRequest'}})
|
||
.then(function(r){ return r.text(); })
|
||
.then(function(html){ body.innerHTML = html; })
|
||
.catch(function(){ body.innerHTML = '<p class="p-3 text-danger">{% translate "Erreur lors du chargement." %}</p>'; });
|
||
});
|
||
})();
|
||
</script>
|
||
{% endif %}
|
||
|
||
{% block linked_assets_block %}
|
||
<!-- Assets liés (parents puis enfants) -->
|
||
{% get_asset_parents asset as parent_groups %}
|
||
{% get_asset_children_tree asset as children_tree %}
|
||
{% if parent_groups or children_tree or allowed_update_fields %}
|
||
<div class="card mb-3" id="linked-assets-card">
|
||
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
||
<h5 class="mb-0">{% translate "Assets liés" %}</h5>
|
||
{% if allowed_update_fields %}
|
||
<button type="button"
|
||
class="btn btn-outline-secondary btn-sm d-none"
|
||
data-edit-mode-only="true"
|
||
id="open-link-asset-modal-btn"
|
||
title="{% translate 'Lier ou créer un asset' %}">
|
||
<i class="bi bi-plus-lg me-1"></i>{% translate "Lier / Créer" %}
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body pb-1" id="linked-assets-body">
|
||
{% if not parent_groups and not children_tree %}
|
||
<p class="text-muted small mb-2" id="no-linked-assets-msg">{% translate "Aucun asset lié." %}</p>
|
||
{% endif %}
|
||
{% include "assets/components/asset_children_tree.html" with children_tree=parent_groups editable=allowed_update_fields parent_model=asset_model parent_id=asset.id %}
|
||
{% include "assets/components/asset_children_tree.html" with children_tree=children_tree editable=allowed_update_fields parent_model=asset_model parent_id=asset.id %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% endblock linked_assets_block %}
|
||
|
||
<!-- Historique des remplacements -->
|
||
{% if asset_replacements and asset_replacements|length > 1 %}
|
||
<div class="card mb-3">
|
||
<div class="card-header bg-light">
|
||
<h5 class="mb-0">{% translate "Historique des remplacements" %}</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- Chaîne de remplacement visuelle -->
|
||
<div class="replacement-chain mb-4">
|
||
{% for replacement in asset_replacements %}
|
||
<div class="replacement-item d-inline-flex align-items-center me-2 mb-2">
|
||
<div class="replacement-badge {% if replacement.id == asset.id %}current{% endif %}">
|
||
{% if thematic_code %}
|
||
<a href="{% url 'assets:'|add:thematic_code|add:'_assets_detail' asset_model replacement.id %}"
|
||
class="text-decoration-none text-reset">
|
||
<strong>{{ replacement.code }}</strong>
|
||
</a>
|
||
{% else %}
|
||
<strong>{{ replacement.code }}</strong>
|
||
{% endif %}
|
||
</div>
|
||
{% if not forloop.last %}
|
||
<div class="replacement-arrow ms-2 me-2">
|
||
<i class="bi bi-arrow-right"></i>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<!-- Tableau détaillé -->
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-striped mb-0 align-middle">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th style="width: 20%;">{% translate "Code" %}</th>
|
||
<th style="width: 15%;">{% translate "Statut" %}</th>
|
||
<th style="width: 20%;">{% translate "Installé le" %}</th>
|
||
<th style="width: 20%;">{% translate "Désinstallé le" %}</th>
|
||
<th style="width: 25%;">{% translate "Durée" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for replacement in asset_replacements %}
|
||
<tr {% if replacement.id == asset.id %}class="table-info fw-bold"{% endif %}>
|
||
<td>
|
||
{% if thematic_code %}
|
||
<a href="{% url 'assets:'|add:thematic_code|add:'_assets_detail' asset_model replacement.id %}">
|
||
{{ replacement.code }}
|
||
</a>
|
||
{% else %}
|
||
{{ replacement.code }}
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if replacement.status == 'active' %}
|
||
<span class="badge bg-success">{{ replacement.get_status_display }}</span>
|
||
{% elif replacement.status == 'damaged' %}
|
||
<span class="badge bg-warning">{{ replacement.get_status_display }}</span>
|
||
{% elif replacement.status == 'to_replace' %}
|
||
<span class="badge bg-danger">{{ replacement.get_status_display }}</span>
|
||
{% elif replacement.status == 'removed' %}
|
||
<span class="badge bg-secondary">{{ replacement.get_status_display }}</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary">{{ replacement.get_status_display }}</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ replacement.installation_date|date:"d/m/Y H:i"|default:"-" }}</td>
|
||
<td>{{ replacement.uninstallation_date|date:"d/m/Y H:i"|default:"-" }}</td>
|
||
<td>
|
||
{% if replacement.installation_date and replacement.uninstallation_date %}
|
||
{% with duration=replacement.uninstallation_date|timeuntil:replacement.installation_date %}
|
||
<small class="text-muted">{{ duration }}</small>
|
||
{% endwith %}
|
||
{% elif replacement.installation_date %}
|
||
<small class="text-muted">{% translate "En service" %}</small>
|
||
{% else %}
|
||
<small class="text-muted">-</small>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Colonne latérale : modèle et localisation -->
|
||
<div class="{% if mobile_view %}col-12{% else %}col-md-4{% endif %}">
|
||
<!-- Modèle associé -->
|
||
{% block model_card %}
|
||
{% asset_model_card asset %}
|
||
{% endblock model_card %}
|
||
|
||
{% block extra_side_cards %}{% endblock extra_side_cards %}
|
||
|
||
<!-- Localisation associée -->
|
||
{% asset_location_link asset %}
|
||
|
||
<!-- Carte et géolocalisation -->
|
||
<div class="card mb-3">
|
||
<div class="card-header bg-light {% if mobile_view %}d-flex flex-column gap-2{% else %}d-flex justify-content-between align-items-center{% endif %}">
|
||
<h5 class="mb-0">{% translate "Géolocalisation" %}</h5>
|
||
{% if asset.lon and asset.lat %}
|
||
{% load l10n %}
|
||
<div class="d-flex gap-2{% if mobile_view %} w-100{% endif %}"{% if mobile_view %} style="justify-content: center;"{% endif %}>
|
||
<a href="https://www.google.com/maps/@{% localize off %}{{ asset.lat }},{{ asset.lon }}{% endlocalize %},18z?layer=c&cbll={% localize off %}{{ asset.lat }},{{ asset.lon }}{% endlocalize %}&cbp=0,0,0,0,0"
|
||
target="_blank"
|
||
class="btn btn-outline-primary btn-sm"
|
||
title="{% translate 'Voir sur Street View' %}">
|
||
<i class="bi bi-person-walking"></i> Street View
|
||
</a>
|
||
<a href="https://www.mapillary.com/app/?lat={% localize off %}{{ asset.lat }}{% endlocalize %}&lng={% localize off %}{{ asset.lon }}{% endlocalize %}&z=17"
|
||
target="_blank"
|
||
class="btn btn-outline-secondary btn-sm"
|
||
title="{% translate 'Voir sur Mapillary' %}">
|
||
<i class="bi bi-images"></i> Mapillary
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<!-- Carte MapLibre -->
|
||
<div id="asset-map" style="height: 400px; width: 100%;"></div>
|
||
|
||
<!-- Coordonnées -->
|
||
<div class="p-3 border-top">
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<div class="asset-editable-field" data-field="lon" data-can-edit="{% if 'lon' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<label class="small text-muted">{% translate "Longitude" %}:</label>
|
||
<div class="field-display">
|
||
<span class="field-value fw-medium">{{ asset.lon|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="number" step="0.000001" class="form-control form-control-sm" data-coordinate="lon" value="{{ lon_display|default:'' }}"></input>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6">
|
||
<div class="asset-editable-field" data-field="lat" data-can-edit="{% if 'lat' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||
<label class="small text-muted">{% translate "Latitude" %}:</label>
|
||
<div class="field-display">
|
||
<span class="field-value fw-medium">{{ asset.lat|default:"-" }}</span>
|
||
</div>
|
||
<div class="field-edit d-none">
|
||
<input type="number" step="0.000001" class="form-control form-control-sm" data-coordinate="lat" value="{{ lat_display|default:'' }}"></input>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% if asset.geom %}
|
||
<div class="mt-2">
|
||
<span id="geometry-display" class="badge bg-secondary">
|
||
{% if asset.geom.geom_type == 'Point' %}
|
||
Point
|
||
{% elif asset.geom.geom_type == 'LineString' %}
|
||
Ligne: {{ asset.geom.num_points }} points
|
||
{% elif asset.geom.geom_type == 'Polygon' %}
|
||
Polygone: {{ asset.geom.num_points }} points
|
||
{% else %}
|
||
{{ asset.geom.geom_type }}
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Blocs supplémentaires en bas de colonne droite -->
|
||
{% block side_bottom_cards %}{% endblock side_bottom_cards %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Interventions liées (pleine largeur) -->
|
||
<div class="row mt-3">
|
||
<div class="col-12">
|
||
{% asset_interventions asset %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Inspections et contrôles liés (pleine largeur) -->
|
||
<div class="row mt-3">
|
||
<div class="col-12">
|
||
{% asset_inspections asset %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Historique des modifications (plein largeur, en bas) -->
|
||
<div class="row mt-3">
|
||
<div class="col-12">
|
||
{% include 'assets/components/asset_history.html' %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Replace Asset Modal -->
|
||
{% if 'replace' in allowed_actions %}
|
||
<div class="modal fade asset-confirm-modal" id="replaceAssetModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">{% translate "Remplacer l'asset" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<form class="replace-asset-form">
|
||
<div class="modal-body">
|
||
<p>{% translate "L'asset actuel sera archivé et un nouvel asset sera créé." %}</p>
|
||
<p class="text-muted small">{% translate "Les noms (FR/NL) de l'ancien asset seront automatiquement copiés." %}</p>
|
||
|
||
{% if asset_models %}
|
||
<div class="mb-3">
|
||
<label class="form-label">{% translate "Modèle du nouvel asset" %}</label>
|
||
<select name="new_model_id" class="form-select">
|
||
<option value="">{% translate "Garder le même modèle" %}</option>
|
||
{% for model in asset_models %}
|
||
<option value="{{ model.id }}">{{ model.get_name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">{% translate "Numéro de série" %} <small class="text-muted">({% translate "optionnel" %})</small></label>
|
||
<input type="text" name="serial_number" class="form-control" placeholder="{% translate 'Numéro de série' %}">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">{% translate "Marque" %} <small class="text-muted">({% translate "optionnel" %})</small></label>
|
||
<input type="text" name="brand" class="form-control" placeholder="{% translate 'Marque' %}">
|
||
</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-warning">
|
||
<i class="bi bi-arrow-repeat"></i> {% translate "Remplacer" %}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Modal : Lier / Créer un asset lié -->
|
||
{% if allowed_update_fields %}
|
||
<div class="modal fade" id="linkAssetModal" tabindex="-1" aria-labelledby="linkAssetModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="linkAssetModalLabel">
|
||
<i class="bi bi-link-45deg me-2"></i>{% translate "Lier / Créer un asset" %}
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<!-- Step 1: Type selector -->
|
||
<div id="link-asset-step-type">
|
||
<div class="mb-3">
|
||
<label class="form-label fw-semibold">{% translate "Type d'asset à lier" %}</label>
|
||
<select class="form-select" id="link-asset-type-select">
|
||
<option value="">{% translate "Chargement..." %}</option>
|
||
</select>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<button type="button" class="btn btn-outline-primary flex-fill" id="link-asset-tab-link">
|
||
<i class="bi bi-link-45deg me-1"></i>{% translate "Lier un asset existant" %}
|
||
</button>
|
||
<button type="button" class="btn btn-outline-success flex-fill" id="link-asset-tab-create">
|
||
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer un nouvel asset" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2a: Link existing -->
|
||
<div id="link-asset-step-link" class="d-none mt-3">
|
||
<h6 class="text-primary"><i class="bi bi-link-45deg me-1"></i>{% translate "Lier un asset existant" %}</h6>
|
||
<div class="mb-3">
|
||
<label class="form-label">{% translate "Rechercher l'asset à lier" %}</label>
|
||
<input type="text" class="form-control" id="link-asset-search-input"
|
||
placeholder="{% translate 'Code ou nom...' %}" autocomplete="off">
|
||
<div id="link-asset-search-results" class="list-group mt-1" style="max-height:240px;overflow-y:auto;"></div>
|
||
</div>
|
||
<div id="link-asset-selected" class="alert alert-info d-none">
|
||
<i class="bi bi-check-circle me-1"></i>
|
||
<span id="link-asset-selected-label"></span>
|
||
<button type="button" class="btn-close float-end" id="link-asset-clear-selected"></button>
|
||
</div>
|
||
<div class="d-flex gap-2 mt-3">
|
||
<button type="button" class="btn btn-secondary" id="link-asset-back-btn">
|
||
<i class="bi bi-arrow-left"></i> {% translate "Retour" %}
|
||
</button>
|
||
<button type="button" class="btn btn-primary ms-auto" id="link-asset-confirm-btn" disabled>
|
||
<i class="bi bi-link-45deg me-1"></i>{% translate "Lier" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2b: Create new -->
|
||
<div id="link-asset-step-create" class="d-none mt-3">
|
||
<h6 class="text-success"><i class="bi bi-plus-circle me-1"></i>{% translate "Créer un nouvel asset" %}</h6>
|
||
<p class="text-muted small">{% translate "Un nouvel asset sera créé et lié automatiquement à cet asset parent." %}</p>
|
||
<div id="link-asset-create-form">
|
||
<!-- Dynamic form fields rendered by JS -->
|
||
</div>
|
||
<div class="d-flex gap-2 mt-3">
|
||
<button type="button" class="btn btn-secondary" id="link-asset-create-back-btn">
|
||
<i class="bi bi-arrow-left"></i> {% translate "Retour" %}
|
||
</button>
|
||
<button type="button" class="btn btn-success ms-auto" id="link-asset-create-confirm-btn">
|
||
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer et lier" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spinner -->
|
||
<div id="link-asset-spinner" class="text-center mt-3 d-none">
|
||
<div class="spinner-border text-primary" role="status"></div>
|
||
</div>
|
||
<!-- Error -->
|
||
<div id="link-asset-error" class="alert alert-danger mt-3 d-none"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
|
||
const assetContainer = document.getElementById('asset-detail-container');
|
||
const PARENT_MODEL = assetContainer ? assetContainer.dataset.assetModel : '';
|
||
const PARENT_ID = assetContainer ? assetContainer.dataset.assetId : '';
|
||
const BASE_URL = '/assets/';
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Modal state */
|
||
/* ------------------------------------------------------------------ */
|
||
let selectedChildType = null; // {model_name, field_on_child|field_name, verbose_name, is_parent}
|
||
let selectedChildAsset = null; // {id, code}
|
||
|
||
const modal = document.getElementById('linkAssetModal');
|
||
const typeSelect = document.getElementById('link-asset-type-select');
|
||
const btnTabLink = document.getElementById('link-asset-tab-link');
|
||
const btnTabCreate = document.getElementById('link-asset-tab-create');
|
||
const stepType = document.getElementById('link-asset-step-type');
|
||
const stepLink = document.getElementById('link-asset-step-link');
|
||
const stepCreate = document.getElementById('link-asset-step-create');
|
||
const searchInput = document.getElementById('link-asset-search-input');
|
||
const searchResults = document.getElementById('link-asset-search-results');
|
||
const selectedBox = document.getElementById('link-asset-selected');
|
||
const selectedLabel = document.getElementById('link-asset-selected-label');
|
||
const clearSelected = document.getElementById('link-asset-clear-selected');
|
||
const confirmLink = document.getElementById('link-asset-confirm-btn');
|
||
const backFromLink = document.getElementById('link-asset-back-btn');
|
||
const backFromCreate = document.getElementById('link-asset-create-back-btn');
|
||
const confirmCreate = document.getElementById('link-asset-create-confirm-btn');
|
||
const spinner = document.getElementById('link-asset-spinner');
|
||
const errorBox = document.getElementById('link-asset-error');
|
||
const openBtn = document.getElementById('open-link-asset-modal-btn');
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Helpers */
|
||
/* ------------------------------------------------------------------ */
|
||
function getCsrfToken() {
|
||
const m = document.cookie.match(/csrftoken=([^;]+)/);
|
||
return m ? m[1] : '';
|
||
}
|
||
|
||
function showSpinner(on) {
|
||
spinner.classList.toggle('d-none', !on);
|
||
}
|
||
|
||
function showError(msg) {
|
||
errorBox.textContent = msg;
|
||
errorBox.classList.remove('d-none');
|
||
}
|
||
|
||
function clearError() {
|
||
errorBox.classList.add('d-none');
|
||
errorBox.textContent = '';
|
||
}
|
||
|
||
function resetModal() {
|
||
selectedChildType = null;
|
||
selectedChildAsset = null;
|
||
childFormFields = [];
|
||
stepType.classList.remove('d-none');
|
||
stepLink.classList.add('d-none');
|
||
stepCreate.classList.add('d-none');
|
||
searchInput.value = '';
|
||
searchResults.innerHTML = '';
|
||
selectedBox.classList.add('d-none');
|
||
confirmLink.disabled = true;
|
||
const formContainer = document.getElementById('link-asset-create-form');
|
||
if (formContainer) formContainer.innerHTML = '';
|
||
clearError();
|
||
showSpinner(false);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Load child types from API */
|
||
/* ------------------------------------------------------------------ */
|
||
async function loadChildTypes() {
|
||
typeSelect.innerHTML = '<option value="">Chargement...</option>';
|
||
try {
|
||
const resp = await fetch(`${BASE_URL}${PARENT_MODEL}/${PARENT_ID}/linked-types/`);
|
||
const data = await resp.json();
|
||
if (!data.success) throw new Error(data.error || 'Erreur');
|
||
typeSelect.innerHTML = '<option value="">— Sélectionner un type —</option>';
|
||
|
||
const childTypes = data.child_types || [];
|
||
const parentTypes = data.parent_types || [];
|
||
|
||
if (childTypes.length > 0) {
|
||
const grpChild = document.createElement('optgroup');
|
||
grpChild.label = '{% translate "Assets enfants" %}';
|
||
childTypes.forEach(ct => {
|
||
const opt = document.createElement('option');
|
||
opt.value = JSON.stringify({
|
||
model_name: ct.model_name,
|
||
field_on_child: ct.field_on_child,
|
||
verbose_name: ct.verbose_name,
|
||
is_parent: false
|
||
});
|
||
opt.textContent = ct.verbose_name_plural;
|
||
grpChild.appendChild(opt);
|
||
});
|
||
typeSelect.appendChild(grpChild);
|
||
}
|
||
|
||
if (parentTypes.length > 0) {
|
||
const grpParent = document.createElement('optgroup');
|
||
grpParent.label = '{% translate "Assets parents" %}';
|
||
parentTypes.forEach(pt => {
|
||
const opt = document.createElement('option');
|
||
opt.value = JSON.stringify({
|
||
model_name: pt.model_name,
|
||
field_name: pt.field_name,
|
||
verbose_name: pt.verbose_name,
|
||
current_id: pt.current_id,
|
||
current_code: pt.current_code,
|
||
is_parent: true
|
||
});
|
||
// Show current parent code if set
|
||
opt.textContent = pt.verbose_name + (pt.current_code ? ' (actuel : ' + pt.current_code + ')' : '');
|
||
grpParent.appendChild(opt);
|
||
});
|
||
typeSelect.appendChild(grpParent);
|
||
}
|
||
|
||
if (typeSelect.options.length === 1) {
|
||
typeSelect.innerHTML = '<option value="">Aucun type disponible</option>';
|
||
btnTabLink.disabled = true;
|
||
btnTabCreate.disabled = true;
|
||
}
|
||
} catch(e) {
|
||
typeSelect.innerHTML = '<option value="">Erreur de chargement</option>';
|
||
}
|
||
}
|
||
|
||
// Show/hide the "Create" button depending on whether a parent type is selected
|
||
typeSelect.addEventListener('change', () => {
|
||
const t = getSelectedType();
|
||
if (t && t.is_parent) {
|
||
btnTabCreate.classList.add('d-none');
|
||
} else {
|
||
btnTabCreate.classList.remove('d-none');
|
||
}
|
||
});
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Open modal entry point */
|
||
/* ------------------------------------------------------------------ */
|
||
if (openBtn) {
|
||
openBtn.addEventListener('click', () => {
|
||
resetModal();
|
||
loadChildTypes();
|
||
new bootstrap.Modal(modal).show();
|
||
});
|
||
}
|
||
|
||
// Also listen for per-group "add" buttons (rendered in asset_children_tree)
|
||
document.addEventListener('click', function(e) {
|
||
const btn = e.target.closest('[data-action="add-linked-asset"]');
|
||
if (!btn) return;
|
||
resetModal();
|
||
loadChildTypes().then(() => {
|
||
// Pre-select the type for this group
|
||
const childModel = btn.dataset.childModel;
|
||
const fieldOnChild = btn.dataset.fieldOnChild;
|
||
for (const opt of typeSelect.options) {
|
||
if (!opt.value) continue;
|
||
const parsed = JSON.parse(opt.value);
|
||
if (parsed.model_name === childModel && parsed.field_on_child === fieldOnChild) {
|
||
typeSelect.value = opt.value;
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
new bootstrap.Modal(modal).show();
|
||
});
|
||
|
||
// Listen for per-group "set parent" buttons
|
||
document.addEventListener('click', function(e) {
|
||
const btn = e.target.closest('[data-action="set-parent-asset"]');
|
||
if (!btn) return;
|
||
resetModal();
|
||
const fieldName = btn.dataset.fieldName;
|
||
const parentModelName = btn.dataset.parentModelName;
|
||
loadChildTypes().then(() => {
|
||
// Pre-select the matching parent type option
|
||
for (const opt of typeSelect.options) {
|
||
if (!opt.value) continue;
|
||
try {
|
||
const parsed = JSON.parse(opt.value);
|
||
if (parsed.is_parent && parsed.field_name === fieldName && parsed.model_name === parentModelName) {
|
||
typeSelect.value = opt.value;
|
||
btnTabCreate.classList.add('d-none');
|
||
// Jump straight to the link step
|
||
selectedChildType = parsed;
|
||
const searchLabel = stepLink.querySelector('label');
|
||
if (searchLabel) searchLabel.textContent = `{% translate "Rechercher le parent à lier" %}`;
|
||
stepType.classList.add('d-none');
|
||
stepLink.classList.remove('d-none');
|
||
if (parsed.current_id) {
|
||
searchInput.value = parsed.current_code || '';
|
||
doSearch();
|
||
}
|
||
break;
|
||
}
|
||
} catch(_e) {}
|
||
}
|
||
});
|
||
new bootstrap.Modal(modal).show();
|
||
});
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Tab selection */
|
||
/* ------------------------------------------------------------------ */
|
||
function getSelectedType() {
|
||
if (!typeSelect.value) return null;
|
||
try { return JSON.parse(typeSelect.value); } catch(_e) { return null; }
|
||
}
|
||
|
||
btnTabLink.addEventListener('click', () => {
|
||
selectedChildType = getSelectedType();
|
||
if (!selectedChildType) { showError('Veuillez sélectionner un type d\'asset.'); return; }
|
||
clearError();
|
||
// Update search label to show it's a parent if needed
|
||
const searchLabel = stepLink.querySelector('label');
|
||
if (searchLabel) {
|
||
searchLabel.textContent = selectedChildType.is_parent
|
||
? `{% translate "Rechercher le parent à lier" %}`
|
||
: `{% translate "Rechercher l'asset à lier" %}`;
|
||
}
|
||
stepType.classList.add('d-none');
|
||
stepLink.classList.remove('d-none');
|
||
// Pre-fill search if there's an existing parent
|
||
if (selectedChildType.is_parent && selectedChildType.current_id) {
|
||
searchInput.value = selectedChildType.current_code || '';
|
||
doSearch();
|
||
}
|
||
});
|
||
|
||
btnTabCreate.addEventListener('click', async () => {
|
||
selectedChildType = getSelectedType();
|
||
if (!selectedChildType) { showError('Veuillez sélectionner un type d\'asset.'); return; }
|
||
clearError();
|
||
stepType.classList.add('d-none');
|
||
stepCreate.classList.remove('d-none');
|
||
showSpinner(true);
|
||
await loadChildForm(selectedChildType.model_name);
|
||
showSpinner(false);
|
||
});
|
||
|
||
backFromLink.addEventListener('click', () => {
|
||
stepLink.classList.add('d-none');
|
||
stepType.classList.remove('d-none');
|
||
clearError();
|
||
});
|
||
|
||
backFromCreate.addEventListener('click', () => {
|
||
stepCreate.classList.add('d-none');
|
||
stepType.classList.remove('d-none');
|
||
clearError();
|
||
});
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Search autocomplete */
|
||
/* ------------------------------------------------------------------ */
|
||
let searchTimer = null;
|
||
searchInput.addEventListener('input', () => {
|
||
clearTimeout(searchTimer);
|
||
searchTimer = setTimeout(doSearch, 300);
|
||
});
|
||
searchInput.addEventListener('focus', () => {
|
||
clearTimeout(searchTimer);
|
||
searchTimer = setTimeout(doSearch, 100);
|
||
});
|
||
|
||
async function doSearch() {
|
||
if (!selectedChildType) return;
|
||
const q = searchInput.value.trim();
|
||
searchResults.innerHTML = '';
|
||
try {
|
||
let url;
|
||
if (selectedChildType.is_parent) {
|
||
// Search in the parent model; no field_on_child filter needed
|
||
url = `${BASE_URL}api/search-assets/${selectedChildType.model_name}/?q=${encodeURIComponent(q)}&limit=10`;
|
||
} else {
|
||
url = `${BASE_URL}api/search-assets/${selectedChildType.model_name}/?q=${encodeURIComponent(q)}&field_on_child=${encodeURIComponent(selectedChildType.field_on_child)}&parent_id=${PARENT_ID}`;
|
||
}
|
||
const resp = await fetch(url);
|
||
const data = await resp.json();
|
||
(data.results || []).forEach(r => {
|
||
const item = document.createElement('button');
|
||
item.type = 'button';
|
||
item.className = 'list-group-item list-group-item-action py-1 px-2';
|
||
item.innerHTML = `<strong>${r.code}</strong>${r.name ? ' — ' + r.name : ''}`;
|
||
item.addEventListener('click', () => selectAsset(r));
|
||
searchResults.appendChild(item);
|
||
});
|
||
if (!data.results || data.results.length === 0) {
|
||
searchResults.innerHTML = '<div class="list-group-item text-muted small">Aucun résultat</div>';
|
||
}
|
||
} catch(e) {
|
||
showError('Erreur lors de la recherche.');
|
||
}
|
||
}
|
||
|
||
function selectAsset(r) {
|
||
selectedChildAsset = r;
|
||
selectedLabel.textContent = `${r.code}${r.name ? ' — ' + r.name : ''}`;
|
||
selectedBox.classList.remove('d-none');
|
||
confirmLink.disabled = false;
|
||
searchResults.innerHTML = '';
|
||
searchInput.value = '';
|
||
}
|
||
|
||
clearSelected.addEventListener('click', () => {
|
||
selectedChildAsset = null;
|
||
selectedBox.classList.add('d-none');
|
||
confirmLink.disabled = true;
|
||
});
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Dynamic form: load fields + render */
|
||
/* ------------------------------------------------------------------ */
|
||
let childFormFields = [];
|
||
|
||
async function loadChildForm(modelName) {
|
||
childFormFields = [];
|
||
const container = document.getElementById('link-asset-create-form');
|
||
container.innerHTML = '<p class="text-muted small">{% translate "Chargement du formulaire…" %}</p>';
|
||
try {
|
||
const url = `${BASE_URL}api/child-asset-form-fields/${modelName}/`
|
||
+ `?field_on_child=${encodeURIComponent(selectedChildType.field_on_child)}`
|
||
+ `&parent_model=${encodeURIComponent(PARENT_MODEL)}`
|
||
+ `&parent_id=${encodeURIComponent(PARENT_ID)}`;
|
||
const resp = await fetch(url);
|
||
const data = await resp.json();
|
||
childFormFields = data.fields || [];
|
||
renderChildForm(container, childFormFields);
|
||
} catch {
|
||
container.innerHTML = '<p class="text-danger small">{% translate "Erreur lors du chargement du formulaire." %}</p>';
|
||
}
|
||
}
|
||
|
||
function renderChildForm(container, fields) {
|
||
container.innerHTML = '';
|
||
if (fields.length === 0) {
|
||
container.innerHTML = '<p class="text-muted small">{% translate "Aucun champ supplémentaire requis." %}</p>';
|
||
return;
|
||
}
|
||
fields.forEach(f => {
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'mb-2';
|
||
|
||
const lbl = document.createElement('label');
|
||
lbl.className = 'form-label small mb-1';
|
||
lbl.setAttribute('for', 'child-field-' + f.name);
|
||
lbl.textContent = f.label;
|
||
if (f.required) {
|
||
const star = document.createElement('span');
|
||
star.className = 'text-danger ms-1';
|
||
star.textContent = '*';
|
||
lbl.appendChild(star);
|
||
}
|
||
wrap.appendChild(lbl);
|
||
|
||
let input;
|
||
if (f.type === 'fk' || f.type === 'choice') {
|
||
input = document.createElement('select');
|
||
input.className = 'form-select form-select-sm';
|
||
const blank = document.createElement('option');
|
||
blank.value = '';
|
||
blank.textContent = f.required ? '{% translate "— Sélectionner —" %}' : '{% translate "— Aucun —" %}';
|
||
input.appendChild(blank);
|
||
(f.choices || []).forEach(c => {
|
||
const opt = document.createElement('option');
|
||
opt.value = c.id;
|
||
opt.textContent = c.label;
|
||
input.appendChild(opt);
|
||
});
|
||
if (f.prefill != null) {
|
||
input.value = String(f.prefill);
|
||
// If the value wasn't matched (prefill not in choices), reset to blank
|
||
if (input.value !== String(f.prefill)) input.value = '';
|
||
}
|
||
} else if (f.type === 'boolean') {
|
||
const checkWrap = document.createElement('div');
|
||
checkWrap.className = 'form-check';
|
||
input = document.createElement('input');
|
||
input.type = 'checkbox';
|
||
input.className = 'form-check-input';
|
||
input.checked = !!f.prefill;
|
||
checkWrap.appendChild(input);
|
||
wrap.appendChild(checkWrap);
|
||
input.id = 'child-field-' + f.name;
|
||
input.name = f.name;
|
||
lbl.setAttribute('for', 'child-field-' + f.name);
|
||
container.appendChild(wrap);
|
||
return;
|
||
} else if (f.type === 'datetime') {
|
||
input = document.createElement('input');
|
||
input.type = 'datetime-local';
|
||
input.className = 'form-control form-control-sm';
|
||
} else if (f.type === 'number') {
|
||
input = document.createElement('input');
|
||
input.type = 'number';
|
||
input.className = 'form-control form-control-sm';
|
||
} else {
|
||
input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.className = 'form-control form-control-sm';
|
||
if (f.prefill != null) input.value = f.prefill;
|
||
}
|
||
|
||
input.id = 'child-field-' + f.name;
|
||
input.name = f.name;
|
||
if (f.required) {
|
||
input.required = true;
|
||
input.dataset.fieldLabel = f.label;
|
||
}
|
||
wrap.appendChild(input);
|
||
container.appendChild(wrap);
|
||
});
|
||
}
|
||
|
||
function collectChildFormData() {
|
||
const container = document.getElementById('link-asset-create-form');
|
||
const data = {};
|
||
container.querySelectorAll('[name]').forEach(el => {
|
||
const name = el.name;
|
||
if (el.type === 'checkbox') {
|
||
data[name] = el.checked;
|
||
} else if (el.value !== '') {
|
||
data[name] = (el.tagName === 'SELECT' || el.type === 'number')
|
||
? (isNaN(Number(el.value)) ? el.value : Number(el.value))
|
||
: el.value;
|
||
}
|
||
});
|
||
return data;
|
||
}
|
||
|
||
function validateChildForm() {
|
||
const container = document.getElementById('link-asset-create-form');
|
||
const missing = [];
|
||
container.querySelectorAll('[required]').forEach(el => {
|
||
el.classList.remove('is-invalid');
|
||
if (!el.value) {
|
||
el.classList.add('is-invalid');
|
||
missing.push(el.dataset.fieldLabel || el.name);
|
||
}
|
||
});
|
||
return missing;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* DOM helpers: add / remove linked asset rows without reload */
|
||
/* ------------------------------------------------------------------ */
|
||
function _makeStatusBadge(status) {
|
||
const labels = {
|
||
active: '{% translate "Actif" %}',
|
||
damaged: '{% translate "Endommagé" %}',
|
||
to_replace: '{% translate "A remplacer" %}',
|
||
removed: '{% translate "Enlevé" %}',
|
||
};
|
||
const classes = {
|
||
active: 'bg-success', damaged: 'bg-warning',
|
||
to_replace: 'bg-danger', removed: 'bg-secondary',
|
||
};
|
||
const cls = classes[status] || 'bg-secondary';
|
||
const lbl = labels[status] || status;
|
||
return `<span class="badge ${cls} ms-1" style="font-size:0.65rem;">${lbl}</span>`;
|
||
}
|
||
|
||
function appendLinkedAssetRow(childModelName, fieldOnChild, childId, childCode, childName, childUrl) {
|
||
// Find the group card matching this child type + field
|
||
const card = document.querySelector(
|
||
`.card[data-group-model="${childModelName}"][data-field-on-child="${fieldOnChild}"]`
|
||
);
|
||
|
||
const li = document.createElement('li');
|
||
li.className = 'list-group-item py-2 px-3';
|
||
li.dataset.itemId = childId;
|
||
li.dataset.itemModel = childModelName;
|
||
li.dataset.itemField = fieldOnChild;
|
||
|
||
const codeHtml = childUrl
|
||
? `<a href="${childUrl}" class="fw-medium text-decoration-none">${childCode}</a>`
|
||
: `<span class="fw-medium">${childCode}</span>`;
|
||
const nameHtml = childName ? `<small class="text-muted">${childName}</small>` : '';
|
||
const badge = _makeStatusBadge('active');
|
||
|
||
li.innerHTML = `
|
||
<div class="d-flex align-items-start gap-2">
|
||
<div class="flex-grow-1">
|
||
<div>${codeHtml}${badge}</div>
|
||
${nameHtml}
|
||
</div>
|
||
<button type="button"
|
||
class="btn btn-outline-danger btn-sm py-0 px-1 flex-shrink-0"
|
||
data-edit-mode-only="true"
|
||
data-action="unlink-child-asset"
|
||
data-child-model="${childModelName}"
|
||
data-child-id="${childId}"
|
||
data-field-on-child="${fieldOnChild}"
|
||
data-parent-model="${PARENT_MODEL}"
|
||
data-parent-id="${PARENT_ID}"
|
||
title="{% translate 'Délier cet asset' %}">
|
||
<i class="bi bi-x-lg" style="font-size:0.7rem;"></i>
|
||
</button>
|
||
</div>`;
|
||
|
||
if (card) {
|
||
// Add the row to the existing group
|
||
const ul = card.querySelector('ul.list-group');
|
||
ul.appendChild(li);
|
||
// Update the count badge
|
||
const badge = card.querySelector('.badge.bg-secondary');
|
||
if (badge) badge.textContent = ul.querySelectorAll('li').length;
|
||
} else {
|
||
// Group doesn't exist yet — create a new card
|
||
const body = document.getElementById('linked-assets-body');
|
||
const newCard = document.createElement('div');
|
||
newCard.className = 'card border mb-2';
|
||
newCard.dataset.groupModel = childModelName;
|
||
newCard.dataset.groupIsParent = 'false';
|
||
newCard.dataset.fieldOnChild = fieldOnChild;
|
||
newCard.innerHTML = `
|
||
<div class="card-header py-2 bg-light d-flex align-items-center gap-2">
|
||
<i class="bi bi-diagram-2 text-muted small"></i>
|
||
<h6 class="mb-0 small fw-semibold">${childModelName}</h6>
|
||
<span class="badge bg-secondary ms-auto">1</span>
|
||
<button type="button"
|
||
class="btn btn-outline-secondary btn-sm"
|
||
data-edit-mode-only="true"
|
||
data-action="add-linked-asset"
|
||
data-child-model="${childModelName}"
|
||
data-field-on-child="${fieldOnChild}"
|
||
data-parent-model="${PARENT_MODEL}"
|
||
data-parent-id="${PARENT_ID}"
|
||
title="{% translate 'Lier un asset existant' %}">
|
||
<i class="bi bi-link-45deg"></i>
|
||
</button>
|
||
</div>
|
||
<ul class="list-group list-group-flush"></ul>`;
|
||
newCard.querySelector('ul').appendChild(li);
|
||
body.appendChild(newCard);
|
||
}
|
||
|
||
// Hide the "no linked assets" message if present
|
||
const noMsg = document.getElementById('no-linked-assets-msg');
|
||
if (noMsg) noMsg.classList.add('d-none');
|
||
|
||
// Ensure new edit-mode-only buttons respect current edit state
|
||
const editModeActive = document.body.dataset.editMode === 'true'
|
||
|| document.querySelector('[data-edit-mode-only]')?.classList.contains('d-none') === false;
|
||
li.querySelectorAll('[data-edit-mode-only]').forEach(el => {
|
||
if (!editModeActive) el.classList.add('d-none');
|
||
});
|
||
}
|
||
|
||
function removeLinkedAssetRow(childModel, childId) {
|
||
const li = document.querySelector(
|
||
`.list-group-item[data-item-id="${childId}"][data-item-model="${childModel}"]`
|
||
);
|
||
if (!li) return;
|
||
const ul = li.closest('ul.list-group');
|
||
const card = li.closest('.card[data-group-model]');
|
||
li.remove();
|
||
if (ul && card) {
|
||
const remaining = ul.querySelectorAll('li').length;
|
||
const badge = card.querySelector('.badge.bg-secondary');
|
||
if (badge) badge.textContent = remaining;
|
||
if (remaining === 0) card.remove();
|
||
}
|
||
}
|
||
|
||
function removeLinkedParentRow(fieldName) {
|
||
const card = document.querySelector(
|
||
`.card[data-group-is-parent="true"][data-field-name="${fieldName}"]`
|
||
);
|
||
if (card) {
|
||
card.remove();
|
||
}
|
||
const body = document.getElementById('linked-assets-body');
|
||
if (body && body.querySelectorAll('.card').length === 0) {
|
||
const noMsg = document.getElementById('no-linked-assets-msg');
|
||
if (noMsg) noMsg.classList.remove('d-none');
|
||
}
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Confirm: Link existing */
|
||
/* ------------------------------------------------------------------ */
|
||
confirmLink.addEventListener('click', async () => {
|
||
if (!selectedChildAsset || !selectedChildType) return;
|
||
clearError();
|
||
showSpinner(true);
|
||
confirmLink.disabled = true;
|
||
try {
|
||
let resp, data;
|
||
if (selectedChildType.is_parent) {
|
||
// Set the parent FK on the current asset
|
||
resp = await fetch(`${BASE_URL}${PARENT_MODEL}/${PARENT_ID}/set-parent/`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
|
||
body: JSON.stringify({
|
||
field_name: selectedChildType.field_name,
|
||
parent_model: selectedChildType.model_name,
|
||
parent_id: selectedChildAsset.id,
|
||
})
|
||
});
|
||
data = await resp.json();
|
||
if (data.success) {
|
||
bootstrap.Modal.getInstance(modal).hide();
|
||
updateParentRow(
|
||
selectedChildType.field_name, selectedChildType.model_name,
|
||
data.parent_id, data.parent_code, data.parent_name || '', data.parent_url
|
||
);
|
||
} else {
|
||
showError(data.error || 'Erreur lors de la liaison.');
|
||
confirmLink.disabled = false;
|
||
}
|
||
} else {
|
||
// Link child to this parent
|
||
resp = await fetch(`${BASE_URL}${PARENT_MODEL}/${PARENT_ID}/link-child/`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
|
||
body: JSON.stringify({
|
||
child_model: selectedChildType.model_name,
|
||
child_id: selectedChildAsset.id,
|
||
field_on_child: selectedChildType.field_on_child,
|
||
})
|
||
});
|
||
data = await resp.json();
|
||
if (data.success) {
|
||
bootstrap.Modal.getInstance(modal).hide();
|
||
appendLinkedAssetRow(
|
||
data.child_model_name, data.field_on_child,
|
||
data.child_id, data.child_code, data.child_name || '', data.child_url
|
||
);
|
||
} else {
|
||
showError(data.error || 'Erreur lors de la liaison.');
|
||
confirmLink.disabled = false;
|
||
}
|
||
}
|
||
} catch(e) {
|
||
showError('Erreur réseau.');
|
||
confirmLink.disabled = false;
|
||
} finally {
|
||
showSpinner(false);
|
||
}
|
||
});
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Confirm: Create and link */
|
||
/* ------------------------------------------------------------------ */
|
||
confirmCreate.addEventListener('click', async () => {
|
||
if (!selectedChildType) return;
|
||
clearError();
|
||
|
||
const missing = validateChildForm();
|
||
if (missing.length > 0) {
|
||
showError('{% translate "Champs obligatoires manquants" %} : ' + missing.join(', '));
|
||
return;
|
||
}
|
||
|
||
showSpinner(true);
|
||
confirmCreate.disabled = true;
|
||
try {
|
||
const body = {
|
||
child_model: selectedChildType.model_name,
|
||
field_on_child: selectedChildType.field_on_child,
|
||
data: collectChildFormData(),
|
||
};
|
||
|
||
const resp = await fetch(`${BASE_URL}${PARENT_MODEL}/${PARENT_ID}/create-child/`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
|
||
body: JSON.stringify(body)
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
bootstrap.Modal.getInstance(modal).hide();
|
||
appendLinkedAssetRow(
|
||
data.child_model_name, data.field_on_child,
|
||
data.child_id, data.child_code, data.child_name || '', data.child_url
|
||
);
|
||
} else {
|
||
showError(data.error || 'Erreur lors de la création.');
|
||
confirmCreate.disabled = false;
|
||
}
|
||
} catch(e) {
|
||
showError('Erreur réseau.');
|
||
confirmCreate.disabled = false;
|
||
} finally {
|
||
showSpinner(false);
|
||
}
|
||
});
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Update parent row after set-parent */
|
||
/* ------------------------------------------------------------------ */
|
||
function updateParentRow(fieldName, parentModelName, parentId, parentCode, parentName, parentUrl) {
|
||
// Find the existing parent group card
|
||
const card = document.querySelector(
|
||
`.card[data-group-is-parent="true"][data-field-name="${fieldName}"]`
|
||
);
|
||
if (!card) return;
|
||
|
||
const ul = card.querySelector('ul.list-group');
|
||
if (!ul) return;
|
||
|
||
// Clear existing item(s) and add the new one
|
||
ul.innerHTML = '';
|
||
|
||
const codeHtml = parentUrl
|
||
? `<a href="${parentUrl}" class="fw-medium text-decoration-none">${parentCode}</a>`
|
||
: `<span class="fw-medium">${parentCode}</span>`;
|
||
const nameHtml = parentName ? `<small class="text-muted">${parentName}</small>` : '';
|
||
|
||
const li = document.createElement('li');
|
||
li.className = 'list-group-item py-2 px-3';
|
||
li.dataset.itemId = parentId;
|
||
li.dataset.itemModel = parentModelName;
|
||
li.dataset.itemField = fieldName;
|
||
li.innerHTML = `
|
||
<div class="d-flex align-items-start gap-2">
|
||
<div class="flex-grow-1">
|
||
<div>${codeHtml}</div>
|
||
${nameHtml}
|
||
</div>
|
||
</div>`;
|
||
ul.appendChild(li);
|
||
|
||
// Update count badge
|
||
const badge = card.querySelector('.badge.bg-secondary');
|
||
if (badge) badge.textContent = 1;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Unlink child asset */
|
||
/* ------------------------------------------------------------------ */
|
||
document.addEventListener('click', async function(e) {
|
||
const btn = e.target.closest('[data-action="unlink-child-asset"]');
|
||
if (!btn) return;
|
||
if (!confirm('{% translate "Délier cet asset de son parent ?" %}')) return;
|
||
|
||
const childModel = btn.dataset.childModel;
|
||
const childId = btn.dataset.childId;
|
||
const fieldOnChild = btn.dataset.fieldOnChild;
|
||
const parentModel = btn.dataset.parentModel;
|
||
const parentId = btn.dataset.parentId;
|
||
|
||
try {
|
||
const resp = await fetch(`${BASE_URL}${parentModel}/${parentId}/unlink-child/`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
|
||
body: JSON.stringify({
|
||
child_model: childModel,
|
||
child_id: parseInt(childId),
|
||
field_on_child: fieldOnChild,
|
||
})
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
removeLinkedAssetRow(childModel, childId);
|
||
} else {
|
||
alert(data.error || 'Erreur lors de la déliason.');
|
||
}
|
||
} catch(e) {
|
||
alert('Erreur réseau.');
|
||
}
|
||
});
|
||
|
||
/* Unlink parent asset */
|
||
document.addEventListener('click', async function(e) {
|
||
const btn = e.target.closest('[data-action="unlink-parent-asset"]');
|
||
if (!btn) return;
|
||
if (!confirm('{% translate "Délier cet asset de son parent ?" %}')) return;
|
||
|
||
const fieldName = btn.dataset.fieldName;
|
||
const parentModel = btn.dataset.parentModel;
|
||
const parentId = btn.dataset.parentId;
|
||
const childModel = btn.dataset.childModel;
|
||
const childId = btn.dataset.childId;
|
||
|
||
try {
|
||
const resp = await fetch(`${BASE_URL}${childModel}/${childId}/set-parent/`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
|
||
body: JSON.stringify({
|
||
field_name: fieldName,
|
||
parent_model: parentModel,
|
||
parent_id: null,
|
||
})
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
removeLinkedParentRow(fieldName);
|
||
} else {
|
||
alert(data.error || 'Erreur lors de la déliason.');
|
||
}
|
||
} catch(e) {
|
||
alert('Erreur réseau.');
|
||
}
|
||
});
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Reset modal on close */
|
||
/* ------------------------------------------------------------------ */
|
||
if (modal) {
|
||
modal.addEventListener('hidden.bs.modal', resetModal);
|
||
}
|
||
})();
|
||
</script>
|
||
{% endif %}
|
||
|
||
{% if intersection_assets_geojson %}
|
||
{{ intersection_assets_geojson|json_script:"intersection-assets-geojson" }}
|
||
{% endif %}
|
||
|
||
{% if connection_lines_geojson %}
|
||
{{ connection_lines_geojson|json_script:"connection-lines-geojson" }}
|
||
{% endif %}
|
||
|
||
{% endblock content %}
|
||
|
||
{% block script %}
|
||
<!-- MapLibre GL 5.7.0 -->
|
||
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
||
<script src="{% static 'common/layer-switcher-control.js' %}?v=20261010_order"></script>
|
||
<script src="{% static 'assets/asset_cms_edit_mode.js' %}"></script>
|
||
<script src="{% static 'assets/asset_location_edit.js' %}"></script>
|
||
<!-- MapLibre map initialization for asset detail -->
|
||
<script src="{% static 'assets/asset_generic_map.js' %}?v=20261010_order"></script>
|
||
<!-- MapboxDraw for geometry editing (Point, LineString, Polygon) -->
|
||
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
||
<script src="{% static 'assets/asset_geometry_edit.js' %}?v=1.1"></script>
|
||
{% endblock script %}
|
||
|
||
{% block extra_scripts %}
|
||
<!-- MapLibre GL 5.7.0 -->
|
||
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
||
<script src="{% static 'common/layer-switcher-control.js' %}?v=20261010_order"></script>
|
||
<script src="{% static 'assets/asset_cms_edit_mode.js' %}"></script>
|
||
<script src="{% static 'assets/asset_location_edit.js' %}"></script>
|
||
<!-- MapLibre map initialization for asset detail -->
|
||
<script src="{% static 'assets/asset_generic_map.js' %}?v=20261010_order"></script>
|
||
<!-- MapboxDraw for geometry editing (Point, LineString, Polygon) -->
|
||
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
||
<script src="{% static 'assets/asset_geometry_edit.js' %}?v=1.1"></script>
|
||
{% endblock extra_scripts %} |