677 lines
No EOL
36 KiB
HTML
677 lines
No EOL
36 KiB
HTML
{% load i18n %}
|
|
{% load static %}
|
|
{% load roles %}
|
|
{% load short_name %}
|
|
|
|
<!DOCTYPE html>
|
|
{% get_current_language as LANGUAGE_CODE %}
|
|
<html lang="{{ LANGUAGE_CODE }}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<link rel="icon" type="image/png" href="{% static 'common/streetup_favicon.png' %}/">
|
|
|
|
<!-- CSS Libraries (local) - Updated Dec 2025 -->
|
|
<!-- Bootstrap 5.3.3 -->
|
|
<link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet">
|
|
<!-- Bootstrap Icons 1.11.3 -->
|
|
<link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet">
|
|
<!-- Bootstrap Table 1.23.5 -->
|
|
<link href="{% static 'common/bootstrap-table.min.css' %}" rel="stylesheet">
|
|
<!-- MapLibre GL 5.7.0 -->
|
|
<link href="{% static 'common/maplibre-gl.css' %}" rel="stylesheet">
|
|
|
|
<link rel="stylesheet" href="{% static 'common/base_style.css' %}?v=1.3">
|
|
<link rel="stylesheet" href="{% static 'common/help_panel.css' %}">
|
|
|
|
<script>
|
|
window.MAP_STYLE_LIGHT = "{{ MAP_STYLE_LIGHT|escapejs }}";
|
|
window.MAP_STYLE_STANDARD = "{{ MAP_STYLE_STANDARD|escapejs }}";
|
|
window.MAP_DEFAULT_CENTER = {{ map_default_center|safe }};
|
|
window.MAP_DEFAULT_ZOOM = {{ map_default_zoom }};
|
|
window.MAP_DEFAULT_EXTENT = {% if map_default_extent %}{{ map_default_extent|safe }}{% else %}null{% endif %};
|
|
window.MAP_ORTHO_WMS_URL = "{{ map_ortho_wms_url|escapejs }}";
|
|
window.MAP_ORTHO_WMS_NAME = "{{ map_ortho_wms_name|escapejs }}";
|
|
window.MAP_ORTHO_WMS_ATTRIBUTION = "{{ map_ortho_wms_attribution|escapejs }}";
|
|
|
|
(function() {
|
|
let _maplibregl = window.maplibregl;
|
|
Object.defineProperty(window, 'maplibregl', {
|
|
get: function() {
|
|
return _maplibregl;
|
|
},
|
|
set: function(val) {
|
|
_maplibregl = val;
|
|
if (_maplibregl && _maplibregl.Map && !_maplibregl.Map._isPatched) {
|
|
const OriginalMap = _maplibregl.Map;
|
|
_maplibregl.Map = function(options) {
|
|
options = options || {};
|
|
|
|
const isDefaultCenter = !options.center ||
|
|
(Array.isArray(options.center) &&
|
|
Math.abs(options.center[0] - 4.3572) < 0.0001 &&
|
|
Math.abs(options.center[1] - 50.8477) < 0.0001);
|
|
|
|
if (isDefaultCenter && window.MAP_DEFAULT_CENTER) {
|
|
options.center = window.MAP_DEFAULT_CENTER;
|
|
}
|
|
|
|
const isDefaultZoom = typeof options.zoom === 'undefined' || options.zoom === 11 || options.zoom === 12;
|
|
if (isDefaultZoom && typeof window.MAP_DEFAULT_ZOOM !== 'undefined') {
|
|
options.zoom = window.MAP_DEFAULT_ZOOM;
|
|
}
|
|
|
|
if (options.maxBounds === undefined && window.MAP_DEFAULT_EXTENT) {
|
|
options.maxBounds = window.MAP_DEFAULT_EXTENT;
|
|
}
|
|
|
|
if (new.target && new.target !== _maplibregl.Map) {
|
|
return Reflect.construct(OriginalMap, [options], new.target);
|
|
}
|
|
return new OriginalMap(options);
|
|
};
|
|
_maplibregl.Map.prototype = OriginalMap.prototype;
|
|
_maplibregl.Map._isPatched = true;
|
|
|
|
const originalAddSource = OriginalMap.prototype.addSource;
|
|
OriginalMap.prototype.addSource = function(id, source) {
|
|
if (id === 'urbis' && source && window.MAP_ORTHO_WMS_URL) {
|
|
if (source.tiles) {
|
|
source.tiles = [window.MAP_ORTHO_WMS_URL];
|
|
}
|
|
if (source.attribution && window.MAP_ORTHO_WMS_ATTRIBUTION) {
|
|
source.attribution = window.MAP_ORTHO_WMS_ATTRIBUTION;
|
|
}
|
|
}
|
|
return originalAddSource.call(this, id, source);
|
|
};
|
|
}
|
|
},
|
|
configurable: true
|
|
});
|
|
if (window.maplibregl) {
|
|
window.maplibregl = window.maplibregl;
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
{% block head %}
|
|
<title>{{ product_name }}</title>
|
|
{% endblock head %}
|
|
{% block extra_head %}{% endblock %}
|
|
{% block extra_css %}{% endblock %}
|
|
<style>
|
|
:root {
|
|
--brand-color: {{ organization_primary_color|default:'#2f3e8b' }};
|
|
}
|
|
.navbar-nav .nav-link,
|
|
#sidebar,
|
|
#sidebar .nav-link {
|
|
color: var(--brand-color) !important;
|
|
}
|
|
.navbar-nav .nav-link:hover,
|
|
#sidebar .nav-link:hover {
|
|
opacity: 0.8;
|
|
}
|
|
nav.gradient-custom {
|
|
border-bottom: 1px solid var(--brand-color) !important;
|
|
}
|
|
#sidebar {
|
|
border-right: 1px solid var(--brand-color) !important;
|
|
}
|
|
#sidebar .nav-link {
|
|
padding: 0.5rem 0.75rem !important;
|
|
border-radius: 8px;
|
|
margin-bottom: 2px;
|
|
}
|
|
@media (min-width: 1200px) {
|
|
#sidebar {
|
|
padding-left: 0.5rem !important;
|
|
padding-right: 0.5rem !important;
|
|
}
|
|
#sidebar .nav-link {
|
|
padding: 0.5rem 1.5rem !important;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar navbar-expand-lg gradient-custom">
|
|
<div class="container-fluid d-flex justify-content-between align-items-center">
|
|
<!-- Logo -->
|
|
<a class="navbar-brand" href="/">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="36" height="36" class="me-2" style="vertical-align: middle; color: var(--brand-color);">
|
|
<!-- Filled brand color circle -->
|
|
<circle cx="12" cy="12" r="11" fill="currentColor" stroke="none" />
|
|
<!-- White road outline (open bottom, SSW entry, both sides touching the circle) -->
|
|
<path d="M5.7 21 C15 18, 2 11, 9.2 7.85 L6.5 6.7 L14.7 2.5 L17.5 11.3 L14.8 10.15 C7 11, 20 18, 9.2 22.6" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
|
|
<!-- White S-curved centerline (SSW entry) -->
|
|
<path d="M7.45 21.8 C17.5 18, 4.5 11, 12 9" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-dasharray="2.5,2.5" stroke-linecap="round" />
|
|
</svg>
|
|
<span style="color: var(--brand-color); font-weight: bold; font-size: 20px;">
|
|
{{ product_name }} - {{ organization_name }}
|
|
</span>
|
|
{% if environment_name %}
|
|
<span class="badge bg-danger ms-2" style="font-size: 12px; vertical-align: middle;">{{ environment_name }}</span>
|
|
{% endif %}
|
|
</a>
|
|
|
|
<!-- Toggler -->
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
|
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
|
|
<!-- Global Search - Centré au milieu de la page (à activer plus tard) -->
|
|
<!--
|
|
<div class="position-absolute start-50 translate-middle-x d-none d-lg-block" style="z-index: 1000;">
|
|
<form role="search" class="d-flex">
|
|
<div class="input-group" style="width: 400px;">
|
|
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
|
|
<input class="form-control border-start-0" type="search" placeholder="{% translate 'Rechercher...' %}" aria-label="Search">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
-->
|
|
|
|
<!-- Navbar Content -->
|
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
|
|
<!-- Right Menu -->
|
|
<ul class="navbar-nav ms-auto d-flex align-items-center gap-3" style="color: var(--brand-color);">
|
|
|
|
|
|
<!-- View Switcher -->
|
|
{% with accessible_views=user.config.get_accessible_views %}
|
|
{% if accessible_views.count > 1 %}
|
|
<li class="nav-item dropdown">
|
|
<button class="nav-link dropdown-toggle" type="button" id="viewSwitcherDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
|
{% if current_view %}
|
|
<i class="bi {{ current_view.icon }}" style="font-size: 1.2rem;"></i> {{ current_view.name }}
|
|
{% else %}
|
|
<i class="bi bi-grid" style="font-size: 1.2rem;"></i> {% translate "Sélectionner une vue" %}
|
|
{% endif %}
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="viewSwitcherDropdown">
|
|
{% for view in accessible_views %}
|
|
<li>
|
|
<a class="dropdown-item {% if current_view and view.code == current_view.code %}active{% endif %}" href="{% url view.url_name %}">
|
|
<i class="bi {{ view.icon }} me-2"></i>{{ view.name }}
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</li>
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<!-- Notifications -->
|
|
<li class="nav-item dropdown">
|
|
<a href="#" class="nav-link position-relative" id="notifDropdown"
|
|
data-bs-toggle="dropdown" aria-expanded="false"
|
|
data-bs-auto-close="outside">
|
|
<i class="bi bi-bell" style="font-size: 1.5rem;"></i>
|
|
<span id="navbar-notif-badge"
|
|
class="position-absolute translate-middle badge rounded-pill"
|
|
style="background-color: var(--brand-color); color: #ffffff; top: 6px; left: 32px;
|
|
{% if not unread_notifications_count %}display:none;{% endif %}">
|
|
{{ unread_notifications_count }}
|
|
<span class="visually-hidden">{% translate "notifications non lues" %}</span>
|
|
</span>
|
|
</a>
|
|
<div class="dropdown-menu dropdown-menu-end p-0 shadow"
|
|
aria-labelledby="notifDropdown"
|
|
style="min-width: 360px; max-width: 420px;">
|
|
<!-- Header -->
|
|
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom bg-light rounded-top">
|
|
<span class="fw-semibold small">{% translate "Notifications" %}</span>
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<button id="markAllReadBtn"
|
|
class="btn btn-link btn-sm p-0 text-muted small"
|
|
title="{% translate 'Tout marquer comme lu' %}">
|
|
<i class="bi bi-check2-all"></i>
|
|
</button>
|
|
<a href="{% url 'notifications:inbox' %}" class="btn btn-link btn-sm p-0 text-muted small">
|
|
{% translate "Voir tout" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<!-- Notification list (populated by JS) -->
|
|
<div id="notif-dropdown-list"
|
|
style="max-height: 380px; overflow-y: auto; overflow-x: hidden;">
|
|
<div class="text-center text-muted py-4 small" id="notif-dropdown-empty">
|
|
<i class="bi bi-bell-slash d-block mb-1" style="font-size:1.5rem;"></i>
|
|
{% translate "Aucune notification non lue" %}
|
|
</div>
|
|
</div>
|
|
<!-- Footer -->
|
|
<div class="border-top bg-light rounded-bottom py-2 px-3 d-flex justify-content-between align-items-center">
|
|
<a href="{% url 'notifications:discussion_list' %}" class="small text-secondary text-decoration-none">
|
|
<i class="bi bi-chat-dots me-1"></i>{% translate "Discussions" %}
|
|
</a>
|
|
<a href="{% url 'notifications:inbox' %}" class="small text-primary text-decoration-none">
|
|
{% translate "Boîte de réception" %}
|
|
<i class="bi bi-arrow-right ms-1"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
|
|
<!-- Help Button -->
|
|
<li class="nav-item">
|
|
<a href="#" class="nav-link" id="helpPanelTrigger" title="{% translate 'Aide & Documentation' %}">
|
|
<i class="bi bi-question-circle" style="font-size: 1.5rem;"></i>
|
|
</a>
|
|
</li>
|
|
|
|
<!-- Language Selector -->
|
|
<li class="nav-item dropdown">
|
|
<form method="post" action="{% url 'set_language' %}" class="d-flex">
|
|
{% csrf_token %}
|
|
<input name="next" type="hidden" value="{{ redirect_to }}">
|
|
<button class="nav-link dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-translate" style="font-size: 1.5rem;"></i>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="languageDropdown">
|
|
{% get_current_language as LANGUAGE_CODE %}
|
|
{% get_available_languages as LANGUAGES %}
|
|
{% get_language_info_list for LANGUAGES as languages %}
|
|
{% for language in languages %}
|
|
<li>
|
|
<button type="submit" name="language" value="{{ language.code }}" class="dropdown-item">
|
|
{{ language.name }}
|
|
</button>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</form>
|
|
</li>
|
|
<li class="nav-item dropdown">
|
|
{% with linked_accounts=switchable_accounts %}
|
|
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" href="#" id="accountDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-box-arrow-right" style="font-size: 1.4rem;"></i>
|
|
<span class="d-none d-lg-inline">{{ user|short_name }}</span>
|
|
</a>
|
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="accountDropdown">
|
|
<li class="dropdown-header text-muted text-uppercase small">{% translate "Mon compte" %}</li>
|
|
<li>
|
|
<a class="dropdown-item" href="{% url 'common:account_info' %}?next={{ request.get_full_path|urlencode }}">{% translate "Gérer mon compte" %}</a>
|
|
</li>
|
|
{% if linked_accounts %}
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li class="dropdown-header text-muted text-uppercase small">{% translate "Changer de compte" %}</li>
|
|
{% for linked_config in linked_accounts %}
|
|
<li>
|
|
<form method="post" action="{% url 'common:switch_account' %}" class="dropdown-item-form">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="target_user_id" value="{{ linked_config.user_id }}">
|
|
<input type="hidden" name="origin_next" value="{{ request.get_full_path }}">
|
|
<button type="submit" class="dropdown-item">
|
|
{{ linked_config.user|short_name }}
|
|
</button>
|
|
</form>
|
|
</li>
|
|
{% endfor %}
|
|
{% endif %}
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li>
|
|
<form method="post" action="{% url 'logout' %}" class="dropdown-item-form">
|
|
{% csrf_token %}
|
|
<button type="submit" class="dropdown-item text-danger">
|
|
<i class="bi bi-box-arrow-right me-2"></i>{% translate "Se déconnecter" %}
|
|
</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
{% endwith %}
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{% block main %}
|
|
<!-- Main container -->
|
|
<div id="main">
|
|
|
|
<!-- Sidebar -->
|
|
<div class="d-flex flex-column sidebar pe-3" id="sidebar">
|
|
{% block sidebar %}
|
|
<a href="/" class="nav-link d-flex align-items-center p-2 {% if request.path == '/' %}active{% endif %}">
|
|
<i class="bi bi-house-fill" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Accueil' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Accueil" %}</span>
|
|
</a>
|
|
|
|
<hr class="my-1">
|
|
|
|
<a href="/assets/" class="nav-link d-flex align-items-center p-2 {% if request.path == '/assets/' or request.path|slice:':14' == '/assets/maps/' %}active{% endif %}">
|
|
<i class="bi bi-layers" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Assets' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Assets" %}</span>
|
|
</a>
|
|
|
|
{% if user.config|has_any_role:"admin,manager,external_manager,controller,observer,operator,editor,top_manager" %}
|
|
<a href="/observations/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':13' == '/observations/' %}active{% endif %}">
|
|
<i class="bi bi-eye" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Observations' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Observations" %}</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<a href="{% url 'inspections:inspections_list' %}" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':12' == '/inspections' %}active{% endif %}">
|
|
<i class="bi bi-search" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Inspections' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Inspections" %}</span>
|
|
</a>
|
|
|
|
<hr class="my-1">
|
|
|
|
{% if user.config|has_any_role:"admin,manager,controller,external_manager,top_manager" %}
|
|
<a href="/contracts/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':11' == '/contracts/' and request.path|slice:':17' != '/contracts/teams/' or request.path|slice:':8' == '/orders/' %}active{% endif %}">
|
|
<i class="bi bi-currency-euro" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Commandes' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Commandes" %}</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% if user.config|has_any_role:"admin,manager,controller,external_manager,top_manager" %}
|
|
<a href="/projects/" class="nav-link d-flex align-items-center p-2 {% if request.path == '/projects/' %}active{% endif %}">
|
|
<i class="bi bi-wrench-adjustable" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Projets' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Projets" %}</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<a href="/interventions/" class="nav-link d-flex align-items-center p-2 {% if request.path == '/interventions/' %}active{% endif %}">
|
|
<i class="bi bi-wrench" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Interventions' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Interventions" %}</span>
|
|
</a>
|
|
|
|
{% if user.config|has_any_role:"admin,manager,controller,external_manager,top_manager" %}
|
|
<a href="/controls/" class="nav-link d-flex align-items-center p-2 {% if request.path == '/controls/' %}active{% endif %}">
|
|
<i class="bi bi-ui-checks" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Contrôles' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Contrôles" %}</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<hr class="my-1">
|
|
|
|
<a href="/interventions/calendar/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':24' == '/interventions/calendar/' %}active{% endif %}">
|
|
<i class="bi bi-calendar2-week" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Calendrier' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Calendrier" %}</span>
|
|
</a>
|
|
|
|
{% if user.config|can_view_teams %}
|
|
<a href="/contracts/teams/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':17' == '/contracts/teams/' %}active{% endif %}">
|
|
<i class="bi bi-people" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Équipes' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Équipes" %}</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% if user.config|has_any_role:"admin,stock_manager,manager,controller,top_manager" %}
|
|
<hr class="my-1">
|
|
<a href="/stock/warehouses/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':18' == '/stock/warehouses/' %}active{% endif %}">
|
|
<i class="bi bi-buildings" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Entrepôts' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Entrepôts" %}</span>
|
|
</a>
|
|
|
|
{% if user.config|has_any_role:"admin,stock_manager,top_manager" %}
|
|
<a href="/stock/movements/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':17' == '/stock/movements/' %}active{% endif %}">
|
|
<i class="bi bi-arrow-repeat" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Flux de stock' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Flux de stock" %}</span>
|
|
</a>
|
|
<a href="/stock/purchases/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':17' == '/stock/purchases/' %}active{% endif %}">
|
|
<i class="bi bi-cart" data-bs-toggle="tooltip" data-bs-placement="right" title="{% translate 'Achats' %}"></i>
|
|
<span class="ms-2 text d-none d-md-inline">{% translate "Achats" %}</span>
|
|
</a>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% endblock sidebar %}
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div id="content-wrapper">
|
|
|
|
{% block content %}
|
|
|
|
{% endblock content%}
|
|
</div>
|
|
|
|
{% include "common/components/help_panel.html" %}
|
|
|
|
</div>
|
|
{% endblock main %}
|
|
|
|
<!-- JS Libraries (local) - Updated Dec 2025 -->
|
|
<!-- Bootstrap 5.3.3 -->
|
|
<script src="{% static 'common/bootstrap.bundle.min.js' %}"></script>
|
|
<!-- jQuery 3.7.1 -->
|
|
<script src="{% static 'common/jquery.min.js' %}"></script>
|
|
<!-- Bootstrap Table 1.23.5 -->
|
|
<script src="{% static 'common/bootstrap-table.min.js' %}"></script>
|
|
<!-- MapLibre GL 5.7.0 -->
|
|
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
|
<!-- MapLibre address search control (uses autocomplete_street) -->
|
|
<script src="{% static 'common/address_search_control.js' %}"></script>
|
|
<!-- Django i18n JavaScript catalog (provides gettext, ngettext, etc.) -->
|
|
<script src="{% url 'javascript-catalog' %}"></script>
|
|
|
|
{% block script %}
|
|
{% endblock script %}
|
|
|
|
<script>
|
|
/* ── Notification dropdown ───────────────────────────────────────────── */
|
|
(function () {
|
|
const CSRF = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';
|
|
const API_RECENT = "{% url 'notifications:api_recent' %}";
|
|
const MARK_READ_BASE = "{% url 'notifications:mark_read' 0 %}";
|
|
const MARK_ALL = "{% url 'notifications:mark_all_read' %}";
|
|
|
|
const dropdown = document.getElementById('notifDropdown');
|
|
const list = document.getElementById('notif-dropdown-list');
|
|
const empty = document.getElementById('notif-dropdown-empty');
|
|
const badge = document.getElementById('navbar-notif-badge');
|
|
|
|
function updateBadge(count) {
|
|
if (badge) {
|
|
if (count > 0) { badge.textContent = count; badge.style.display = ''; }
|
|
else { badge.style.display = 'none'; }
|
|
}
|
|
const homeAlert = document.getElementById('home-unread-notifications-alert');
|
|
if (homeAlert) {
|
|
if (count > 0) {
|
|
const countEls = homeAlert.querySelectorAll('.home-unread-notifications-count');
|
|
countEls.forEach(el => el.textContent = count);
|
|
homeAlert.style.display = '';
|
|
} else {
|
|
homeAlert.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
const TYPE_ICONS = {
|
|
message: 'bi-chat-dots', reminder: 'bi-alarm',
|
|
task: 'bi-check2-square', alert: 'bi-exclamation-triangle', info: 'bi-info-circle',
|
|
};
|
|
const TYPE_COLORS = {
|
|
message: 'primary', reminder: 'warning', task: 'success', alert: 'danger', info: 'info',
|
|
};
|
|
|
|
async function loadRecent() {
|
|
try {
|
|
const resp = await fetch(API_RECENT, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
|
const data = await resp.json();
|
|
updateBadge(data.unread_count);
|
|
renderNotifications(data.notifications);
|
|
} catch (e) { /* silent */ }
|
|
}
|
|
|
|
function renderNotifications(items) {
|
|
// Remove old dynamic items
|
|
list.querySelectorAll('.notif-dd-item').forEach(el => el.remove());
|
|
if (!items || items.length === 0) {
|
|
empty.style.display = '';
|
|
return;
|
|
}
|
|
empty.style.display = 'none';
|
|
items.forEach(n => {
|
|
const icon = TYPE_ICONS[n.notification_type] || 'bi-bell';
|
|
const color = TYPE_COLORS[n.notification_type] || 'secondary';
|
|
const dt = new Date(n.created_at);
|
|
const dateStr = dt.toLocaleDateString('fr-BE') + ' ' + dt.toLocaleTimeString('fr-BE', { hour: '2-digit', minute: '2-digit' });
|
|
const div = document.createElement('div');
|
|
div.className = 'notif-dd-item d-flex gap-2 px-3 py-2 border-bottom notif-dd-entry';
|
|
div.setAttribute('data-pk', n.id);
|
|
div.setAttribute('data-type', n.type);
|
|
div.style.cssText = 'background:#f0f4ff; cursor:default;';
|
|
div.innerHTML = `
|
|
<span class="text-${color} flex-shrink-0 mt-1"><i class="bi ${icon}"></i></span>
|
|
<div class="flex-grow-1 min-w-0">
|
|
<div class="d-flex justify-content-between align-items-start gap-1">
|
|
<span class="fw-semibold small" style="white-space: normal; word-break: break-word;">${n.title}</span>
|
|
<button class="btn btn-link btn-sm p-0 text-muted flex-shrink-0 notif-dd-mark-read"
|
|
data-pk="${n.id}" data-type="${n.type}" title="Marquer comme lu" style="font-size:.8rem;">
|
|
<i class="bi bi-check2"></i>
|
|
</button>
|
|
</div>
|
|
${n.content ? `<p class="small text-muted mb-1" style="white-space: normal; word-break: break-word;">${n.content}</p>` : ''}
|
|
<div class="d-flex gap-2 align-items-center flex-wrap">
|
|
<span class="text-muted" style="font-size:.7rem;">${dateStr}</span>
|
|
${n.team_name ? `<span class="badge bg-secondary" style="font-size:.65rem;">${n.team_name}</span>` : ''}
|
|
${n.action_url ? `<a href="${n.action_url}" class="badge bg-primary text-white text-decoration-none" style="font-size:.65rem;">${n.action_label || '→'}</a>` : ''}
|
|
</div>
|
|
</div>`;
|
|
list.insertBefore(div, empty);
|
|
});
|
|
|
|
// Bind mark-read buttons inside dropdown
|
|
list.querySelectorAll('.notif-dd-mark-read').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
const pk = btn.dataset.pk;
|
|
const type = btn.dataset.type;
|
|
const url = MARK_READ_BASE.replace('0', pk);
|
|
const resp = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRFToken': CSRF, 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: `type=${type}`,
|
|
});
|
|
if (resp.ok) {
|
|
const d = await resp.json();
|
|
updateBadge(d.unread_count);
|
|
loadRecent();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Load when dropdown opens
|
|
if (dropdown) {
|
|
dropdown.addEventListener('show.bs.dropdown', loadRecent);
|
|
}
|
|
|
|
// Mark all read button
|
|
const markAllBtn = document.getElementById('markAllReadBtn');
|
|
if (markAllBtn) {
|
|
markAllBtn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
const resp = await fetch(MARK_ALL, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRFToken': CSRF, 'X-Requested-With': 'XMLHttpRequest' },
|
|
});
|
|
if (resp.ok) {
|
|
const d = await resp.json();
|
|
updateBadge(d.unread_count);
|
|
renderNotifications([]);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Refresh badge every 60s
|
|
setInterval(async () => {
|
|
try {
|
|
const resp = await fetch("{% url 'notifications:api_unread_count' %}", {
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
});
|
|
const d = await resp.json();
|
|
updateBadge(d.unread_count);
|
|
} catch (e) { /* silent */ }
|
|
}, 60000);
|
|
})();
|
|
</script>
|
|
|
|
<script>
|
|
/* ── Bouton d'agrandissement de la carte pour les pages de détail ── */
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Cibler soit #map (intersections/rues) soit #asset-map (assets génériques)
|
|
const mapEl = document.getElementById('map') || document.getElementById('asset-map');
|
|
if (!mapEl) return;
|
|
|
|
// S'assurer qu'on est sur une page de détail (carte dans col-md-4 ou dans une vue mobile col-12)
|
|
const isDetailMap = mapEl.closest('.col-md-4') !== null || mapEl.closest('.col-12') !== null;
|
|
if (!isDetailMap) return;
|
|
|
|
// Créer le bouton
|
|
const toggleBtn = document.createElement('button');
|
|
toggleBtn.className = 'btn btn-light btn-sm map-maximize-btn';
|
|
toggleBtn.style.cssText = 'position: absolute; top: 10px; left: 10px; z-index: 1000; border: 1px solid rgba(0,0,0,0.15); box-shadow: 0 2px 6px rgba(0,0,0,0.25); width: 32px; height: 32px; padding: 0; display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: all 0.2s ease-in-out;';
|
|
toggleBtn.title = '{% translate "Afficher la carte en grand" %}';
|
|
toggleBtn.innerHTML = '<i class="bi bi-arrows-fullscreen" style="font-size: 1.1rem; color: #2f3e8b;"></i>';
|
|
|
|
// Effet au survol
|
|
toggleBtn.addEventListener('mouseenter', () => {
|
|
toggleBtn.style.backgroundColor = '#e2e6ea';
|
|
toggleBtn.style.transform = 'scale(1.05)';
|
|
});
|
|
toggleBtn.addEventListener('mouseleave', () => {
|
|
toggleBtn.style.backgroundColor = '#f8f9fa';
|
|
toggleBtn.style.transform = 'scale(1)';
|
|
});
|
|
|
|
if (getComputedStyle(mapEl).position === 'static') {
|
|
mapEl.style.position = 'relative';
|
|
}
|
|
|
|
mapEl.appendChild(toggleBtn);
|
|
|
|
function toggleMaximize() {
|
|
const isMaximized = mapEl.classList.toggle('maximized');
|
|
|
|
if (isMaximized) {
|
|
toggleBtn.innerHTML = '<i class="bi bi-fullscreen-exit" style="font-size: 1.1rem; color: #2f3e8b;"></i>';
|
|
toggleBtn.title = '{% translate "Réduire la carte" %}';
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
toggleBtn.innerHTML = '<i class="bi bi-arrows-fullscreen" style="font-size: 1.1rem; color: #2f3e8b;"></i>';
|
|
toggleBtn.title = '{% translate "Afficher la carte en grand" %}';
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// Forcer MapLibre à recalculer sa taille après la mise à jour CSS
|
|
setTimeout(() => {
|
|
window.dispatchEvent(new Event('resize'));
|
|
// Supporte l'instance de map globale nommée "map" ou exposée sur "window.map"
|
|
if (window.map && typeof window.map.resize === 'function') {
|
|
window.map.resize();
|
|
} else if (typeof map !== 'undefined' && map && typeof map.resize === 'function') {
|
|
map.resize();
|
|
}
|
|
}, 150);
|
|
}
|
|
|
|
toggleBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
toggleMaximize();
|
|
});
|
|
|
|
// Quitter avec la touche Echap
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && mapEl.classList.contains('maximized')) {
|
|
toggleMaximize();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script src="{% static 'common/help_panel.js' %}"></script>
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html> |