340 lines
18 KiB
HTML
340 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
{% load static i18n %}
|
|
|
|
{% block title %}{% translate "Zones de gestion" %} | {{ product_name }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Breadcrumb & Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-1">
|
|
<li class="breadcrumb-item"><a href="/">{% translate "Accueil" %}</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'assets:index' %}">{% translate "Assets" %}</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page">{% translate "Zones de gestion" %}</li>
|
|
</ol>
|
|
</nav>
|
|
{% if can_create %}
|
|
<a href="{% url 'assets:management_zone_create' %}{% if selected_thematic_code %}?thematic={{ selected_thematic_code }}{% endif %}" class="btn btn-success">
|
|
<i class="bi bi-plus-lg me-1"></i>{% translate "Nouvelle zone de gestion" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Title Bar -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
<i class="bi bi-diagram-3-fill text-success me-2"></i>{% translate "Zones de gestion" %}
|
|
</h1>
|
|
<p class="text-muted mb-0">
|
|
{% translate "Regroupements géographiques et fonctionnels d'équipements pour l'entretien récurrent, les plans de fauche et l'exploitation." %}
|
|
</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<span class="badge bg-light text-dark border fs-6 px-3 py-2">
|
|
<strong>{{ total_count }}</strong> {% translate "zone(s) répertoriée(s)" %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Card -->
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3 align-items-end">
|
|
<!-- Search text -->
|
|
<div class="col-md-3">
|
|
<label class="form-label small text-muted">{% translate "Recherche" %}</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
<input type="text" name="q" class="form-control" placeholder="{% translate 'Code, nom, consignes...' %}" value="{{ search_query }}">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Thematic -->
|
|
<div class="col-md-3">
|
|
<label class="form-label small text-muted">{% translate "Thématique" %}</label>
|
|
<select name="thematic" id="filterThematic" class="form-select form-select-sm">
|
|
<option value="">-- {% translate "Toutes les thématiques" %} --</option>
|
|
{% for th in accessible_thematics %}
|
|
<option value="{{ th.code }}" {% if selected_thematic_code == th.code %}selected{% endif %}>
|
|
{{ th.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Zone Type -->
|
|
<div class="col-md-3">
|
|
<label class="form-label small text-muted">{% translate "Type de zone" %}</label>
|
|
<select name="zone_type" id="filterZoneType" class="form-select form-select-sm">
|
|
<option value="">-- {% translate "Tous les types" %} --</option>
|
|
{% for zt in available_types %}
|
|
<option value="{{ zt.id }}" {% if selected_type_id == zt.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ zt.get_name }} ({{ zt.thematic.name }})
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div class="col-md-2">
|
|
<label class="form-label small text-muted">{% translate "Statut" %}</label>
|
|
<select name="status" class="form-select form-select-sm">
|
|
<option value="">-- {% translate "Tous les statuts" %} --</option>
|
|
<option value="active" {% if selected_status == 'active' %}selected{% endif %}>{% translate "Actives uniquement" %}</option>
|
|
<option value="inactive" {% if selected_status == 'inactive' %}selected{% endif %}>{% translate "Inactives" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Submit / Reset buttons -->
|
|
<div class="col-md-1 d-flex gap-1">
|
|
<button type="submit" class="btn btn-sm btn-primary w-100" title="{% translate 'Filtrer' %}">
|
|
<i class="bi bi-funnel"></i>
|
|
</button>
|
|
<a href="{% url 'assets:management_zones_index' %}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Réinitialiser' %}">
|
|
<i class="bi bi-x-lg"></i>
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Overview Card -->
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center py-2">
|
|
<span class="fw-semibold small">
|
|
<i class="bi bi-map me-1 text-success"></i>{% translate "Aperçu cartographique des périmètres" %}
|
|
</span>
|
|
<small class="text-muted" id="mapLoadingStatus">{% translate "Chargement de la carte..." %}</small>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div id="zonesOverviewMap" style="height: 320px; width: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zones Table Card -->
|
|
<div class="card shadow-sm">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="table-light small text-uppercase text-muted">
|
|
<tr>
|
|
<th style="width: 140px;">{% translate "Code" %}</th>
|
|
<th>{% translate "Nom de la zone" %}</th>
|
|
<th>{% translate "Thématique" %}</th>
|
|
<th>{% translate "Type de zone" %}</th>
|
|
<th>{% translate "Fréquence cible" %}</th>
|
|
<th>{% translate "Saisonnalité" %}</th>
|
|
<th>{% translate "Statut" %}</th>
|
|
<th class="text-end" style="width: 160px;">{% translate "Actions" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in zones_list %}
|
|
{% with zone=item.zone can_edit=item.can_edit %}
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span style="display:inline-block; width: 14px; height: 14px; border-radius: 3px; background-color: {{ zone.get_color }}; border: 1px solid rgba(0,0,0,0.2); flex-shrink: 0;"></span>
|
|
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="fw-bold text-decoration-none">
|
|
{{ zone.code }}
|
|
</a>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-semibold">{{ zone.get_name }}</div>
|
|
{% if zone.instructions %}
|
|
<div class="text-muted small text-truncate" style="max-width: 320px;" title="{{ zone.instructions }}">
|
|
<i class="bi bi-exclamation-triangle text-warning me-1"></i>{{ zone.instructions }}
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary-subtle text-secondary border">
|
|
{{ zone.thematic.name }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
{% if zone.zone_type %}
|
|
<span class="badge" style="background-color: {{ zone.zone_type.default_color }}; color: #fff;">
|
|
{{ zone.zone_type.get_name }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-muted small">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if zone.target_frequency_days %}
|
|
<span class="badge bg-info-subtle text-info border">
|
|
<i class="bi bi-arrow-repeat me-1"></i>{{ zone.target_frequency_days }} {% translate "jours" %}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-muted small">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if zone.seasonality_notes %}
|
|
<span class="small text-muted"><i class="bi bi-calendar3 me-1"></i>{{ zone.seasonality_notes }}</span>
|
|
{% else %}
|
|
<span class="text-muted small">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if zone.is_active %}
|
|
<span class="badge bg-success-subtle text-success border">{% translate "Active" %}</span>
|
|
{% else %}
|
|
<span class="badge bg-danger-subtle text-danger border">{% translate "Inactive" %}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="btn btn-outline-primary" title="{% translate 'Détails' %}">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
{% if can_edit %}
|
|
<a href="{% url 'assets:management_zone_edit' zone.pk %}" class="btn btn-outline-secondary" title="{% translate 'Modifier' %}">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<a href="{% url 'assets:management_zone_delete' zone.pk %}" class="btn btn-outline-danger" title="{% translate 'Supprimer' %}">
|
|
<i class="bi bi-trash"></i>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endwith %}
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="8" class="text-center py-5 text-muted">
|
|
<i class="bi bi-diagram-3 fs-1 d-block mb-2 text-muted"></i>
|
|
<p class="mb-2">{% translate "Aucune zone de gestion ne correspond aux critères sélectionnés." %}</p>
|
|
{% if can_create %}
|
|
<a href="{% url 'assets:management_zone_create' %}{% if selected_thematic_code %}?thematic={{ selected_thematic_code }}{% endif %}" class="btn btn-sm btn-success">
|
|
<i class="bi bi-plus-lg me-1"></i>{% translate "Créer une première zone de gestion" %}
|
|
</a>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock content %}
|
|
|
|
{% block script %}
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const mapDiv = document.getElementById("zonesOverviewMap");
|
|
if (!mapDiv || typeof maplibregl === "undefined") return;
|
|
|
|
const thematicParam = "{{ selected_thematic_code|default:'' }}";
|
|
const geojsonUrl = "{% url 'assets:management_zones_geojson' %}" + (thematicParam ? ("?thematic=" + encodeURIComponent(thematicParam)) : "");
|
|
|
|
const map = new maplibregl.Map({
|
|
container: mapDiv,
|
|
style: window.MAP_STYLE_LIGHT,
|
|
center: window.MAP_DEFAULT_CENTER || [4.3572, 50.8477],
|
|
zoom: 11
|
|
});
|
|
|
|
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
|
|
|
map.on('load', async function () {
|
|
try {
|
|
const resp = await fetch(geojsonUrl, {
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
});
|
|
if (!resp.ok) {
|
|
document.getElementById("mapLoadingStatus").textContent = "";
|
|
return;
|
|
}
|
|
const data = await resp.json();
|
|
const features = data.features || [];
|
|
|
|
document.getElementById("mapLoadingStatus").textContent = `${features.length} {% translate "périmètre(s) affiché(s)" %}`;
|
|
|
|
map.addSource('management-zones-overview', {
|
|
type: 'geojson',
|
|
data: data
|
|
});
|
|
|
|
map.addLayer({
|
|
id: 'management-zones-fill',
|
|
type: 'fill',
|
|
source: 'management-zones-overview',
|
|
paint: {
|
|
'fill-color': ['coalesce', ['get', 'color'], '#198754'],
|
|
'fill-opacity': 0.25
|
|
}
|
|
});
|
|
|
|
map.addLayer({
|
|
id: 'management-zones-line',
|
|
type: 'line',
|
|
source: 'management-zones-overview',
|
|
paint: {
|
|
'line-color': ['coalesce', ['get', 'color'], '#198754'],
|
|
'line-width': 2,
|
|
'line-dasharray': [3, 1.5]
|
|
}
|
|
});
|
|
|
|
const popup = new maplibregl.Popup({ closeButton: true, closeOnClick: true, maxWidth: '300px' });
|
|
|
|
map.on('click', 'management-zones-fill', function (e) {
|
|
if (!e.features || !e.features[0]) return;
|
|
const p = e.features[0].properties;
|
|
const detailUrl = "/assets/management-zones/" + p.id + "/";
|
|
const html = `
|
|
<div style="font-size: 12px; line-height: 1.4;">
|
|
<div style="display:flex; align-items:center; gap:6px; margin-bottom:4px;">
|
|
<span style="display:inline-block; width:12px; height:12px; border-radius:3px; background-color:${p.color || '#198754'};"></span>
|
|
<strong>${p.code}</strong>
|
|
</div>
|
|
<div class="fw-semibold mb-1">${p.name}</div>
|
|
${p.zone_type_name ? `<span class="badge bg-secondary mb-1">${p.zone_type_name}</span>` : ''}
|
|
${p.target_frequency_days ? `<div class="text-muted small"><i class="bi bi-arrow-repeat me-1"></i>${p.target_frequency_days} {% translate "jours" %}</div>` : ''}
|
|
<div class="mt-2 text-end">
|
|
<a href="${detailUrl}" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 11px;">
|
|
{% translate "Consulter la fiche" %} →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
`;
|
|
popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
|
});
|
|
|
|
map.on('mouseenter', 'management-zones-fill', () => { map.getCanvas().style.cursor = 'pointer'; });
|
|
map.on('mouseleave', 'management-zones-fill', () => { map.getCanvas().style.cursor = ''; });
|
|
|
|
// Fit bounds to features
|
|
if (features.length > 0) {
|
|
const bounds = new maplibregl.LngLatBounds();
|
|
features.forEach(f => {
|
|
const coords = f.geometry?.coordinates;
|
|
if (!coords) return;
|
|
const addCoords = (arr) => {
|
|
if (typeof arr[0] === 'number') {
|
|
bounds.extend(arr);
|
|
} else {
|
|
arr.forEach(addCoords);
|
|
}
|
|
};
|
|
addCoords(coords);
|
|
});
|
|
if (!bounds.isEmpty()) {
|
|
map.fitBounds(bounds, { padding: 40, maxZoom: 15 });
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Error loading management zones overview map:", err);
|
|
document.getElementById("mapLoadingStatus").textContent = "";
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock script %}
|