loko/streetup/templates/base.html

655 lines
No EOL
34 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 %}
<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="Accueil"></i>
<span class="ms-2 text d-none d-md-inline">{% translate "Accueil" %}</span>
</a>
<a href="/assets/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':8' == '/assets/' %}active{% endif %}">
<i class="bi bi-layers" data-bs-toggle="tooltip" data-bs-placement="right" title="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="observations"></i>
<span class="ms-2 text d-none d-md-inline">{% translate "Observations" %}</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="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="/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="Projets"></i>
<span class="ms-2 text d-none d-md-inline">{% translate "Projets" %}</span>
</a>
{% endif %}
{% 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="Projets"></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="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="Équipes"></i>
<span class="ms-2 text d-none d-md-inline">{% translate "Équipes" %}</span>
</a>
{% endif %}
{% if user.config|has_any_role:"admin,manager,controller,external_manager,top_manager" %}
<hr class="my-1">
<a href="/contracts/" class="nav-link d-flex align-items-center p-2 {% if request.path|slice:':8' == '/orders/' %}active{% endif %}">
<i class="bi bi-currency-euro" data-bs-toggle="tooltip" data-bs-placement="right" title="Commandes"></i>
<span class="ms-2 text d-none d-md-inline">{% translate "Commandes" %}</span>
</a>
{% endif %}
<hr class="my-1">
{% if user.config|has_any_role:"admin,stock_manager,manager,controller,top_manager" %}
<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="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="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="Achats"></i>
<span class="ms-2 text d-none d-md-inline">{% translate "Achats" %}</span>
</a>
{% endif %}
<hr class="my-1">
{% 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) return;
if (count > 0) { badge.textContent = count; badge.style.display = ''; }
else { badge.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>
</body>
</html>