Compare commits
No commits in common. "main" and "1.2.0" have entirely different histories.
364 changed files with 5555 additions and 47884 deletions
18
Dockerfile
18
Dockerfile
|
|
@ -46,8 +46,15 @@ ENV PROJ_DATA=/opt/conda/envs/loko-env/share/proj
|
||||||
# Argument de build pour activer l'installation des librairies IA (ultralytics, OpenCV...)
|
# Argument de build pour activer l'installation des librairies IA (ultralytics, OpenCV...)
|
||||||
# Passer INSTALL_AI_LIBS=true dans le docker-compose pour les inclure
|
# Passer INSTALL_AI_LIBS=true dans le docker-compose pour les inclure
|
||||||
ARG INSTALL_AI_LIBS=false
|
ARG INSTALL_AI_LIBS=false
|
||||||
|
ARG GIT_COMMIT=unknown
|
||||||
|
ARG BUILD_DATE=unknown
|
||||||
|
ARG LOKO_VERSION=1.0.0
|
||||||
|
|
||||||
# Copie et installation des dépendances Python (mis en cache tant que requirements/ ne change pas)
|
ENV GIT_COMMIT_SHA=${GIT_COMMIT}
|
||||||
|
ENV BUILD_DATE=${BUILD_DATE}
|
||||||
|
ENV LOKO_VERSION=${LOKO_VERSION}
|
||||||
|
|
||||||
|
# Copie et installation des dépendances Python
|
||||||
COPY requirements/ /app/requirements/
|
COPY requirements/ /app/requirements/
|
||||||
RUN pip install --no-cache-dir -r requirements/base.txt && \
|
RUN pip install --no-cache-dir -r requirements/base.txt && \
|
||||||
if [ "$INSTALL_AI_LIBS" = "true" ]; then \
|
if [ "$INSTALL_AI_LIBS" = "true" ]; then \
|
||||||
|
|
@ -101,15 +108,6 @@ COPY loko/road_detection/templates/ /app/road_detection/templates/
|
||||||
COPY loko/*.py /app/
|
COPY loko/*.py /app/
|
||||||
COPY loko/generated_passwords.txt* /app/
|
COPY loko/generated_passwords.txt* /app/
|
||||||
|
|
||||||
# Métadonnées de build dynamiques (placées à la fin pour préserver le cache de build Docker)
|
|
||||||
ARG GIT_COMMIT=unknown
|
|
||||||
ARG BUILD_DATE=unknown
|
|
||||||
ARG LOKO_VERSION=1.0.0
|
|
||||||
|
|
||||||
ENV GIT_COMMIT_SHA=${GIT_COMMIT}
|
|
||||||
ENV BUILD_DATE=${BUILD_DATE}
|
|
||||||
ENV LOKO_VERSION=${LOKO_VERSION}
|
|
||||||
|
|
||||||
# Exposer le port par défaut
|
# Exposer le port par défaut
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -451,10 +451,7 @@ admin.site.register(ComplementaryPoliceRegulation, AssetAdmin)
|
||||||
admin.site.register(SignParkingSpot, AssetAdmin)
|
admin.site.register(SignParkingSpot, AssetAdmin)
|
||||||
|
|
||||||
|
|
||||||
from .models import (
|
from .models import ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract
|
||||||
ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract,
|
|
||||||
ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
|
||||||
)
|
|
||||||
|
|
||||||
admin.site.register(ITSAssetModel, AssetAdmin)
|
admin.site.register(ITSAssetModel, AssetAdmin)
|
||||||
|
|
||||||
|
|
@ -471,44 +468,6 @@ class ITSGeoAssetContractAdmin(AssetAdmin):
|
||||||
list_filter = ("contract",)
|
list_filter = ("contract",)
|
||||||
|
|
||||||
|
|
||||||
class ITSBollardPlotInline(admin.TabularInline):
|
|
||||||
model = ITSBollardPlot
|
|
||||||
extra = 0
|
|
||||||
fields = ("code", "position", "bollard_type", "model", "serial_number", "status")
|
|
||||||
show_change_link = True
|
|
||||||
|
|
||||||
|
|
||||||
class ITSBollardSystemAssetInline(admin.TabularInline):
|
|
||||||
model = ITSBollardSystemAsset
|
|
||||||
extra = 0
|
|
||||||
fields = ("asset", "role", "comment")
|
|
||||||
autocomplete_fields = ("asset",)
|
|
||||||
show_change_link = True
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ITSBollardSystem)
|
|
||||||
class ITSBollardSystemAdmin(AssetGISAdmin, AssetAdmin):
|
|
||||||
search_fields = ("code", "name_fr", "name_nl", "location__code", "location__name_fr")
|
|
||||||
list_display = ("code", "name_fr", "location", "width", "direction", "status", "plots_count")
|
|
||||||
list_filter = ("status", "direction", "location")
|
|
||||||
inlines = [ITSBollardPlotInline, ITSBollardSystemAssetInline]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ITSBollardPlot)
|
|
||||||
class ITSBollardPlotAdmin(AssetGISAdmin, AssetAdmin):
|
|
||||||
search_fields = ("code", "name_fr", "system__code", "serial_number")
|
|
||||||
list_display = ("code", "system", "position", "bollard_type", "model", "status")
|
|
||||||
list_filter = ("bollard_type", "status", "system")
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ITSBollardSystemContract)
|
|
||||||
class ITSBollardSystemContractAdmin(AssetAdmin):
|
|
||||||
search_fields = ("system__code", "contract__contract_number")
|
|
||||||
list_display = ("system", "contract", "status", "maintenance_type")
|
|
||||||
list_filter = ("status", "maintenance_type", "contract")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from .models import ITSLocation, ITSLocationDocument, ITSLocationContract
|
from .models import ITSLocation, ITSLocationDocument, ITSLocationContract
|
||||||
|
|
||||||
admin.site.register(ITSLocation, AssetAdmin)
|
admin.site.register(ITSLocation, AssetAdmin)
|
||||||
|
|
@ -721,34 +680,21 @@ from .models import CleanLitterBinPhoto, CleanLitterBinObservation
|
||||||
from .models import PublicBuilding, BuildingAsset, PublicBuildingDocument
|
from .models import PublicBuilding, BuildingAsset, PublicBuildingDocument
|
||||||
from .models import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
from .models import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
||||||
|
|
||||||
class PublicBuildingDocumentInline(admin.TabularInline):
|
|
||||||
model = PublicBuildingDocument
|
|
||||||
extra = 1
|
|
||||||
fk_name = "building"
|
|
||||||
fields = ("document_type", "floor_location", "file", "description")
|
|
||||||
raw_id_fields = ("floor_location",)
|
|
||||||
|
|
||||||
@admin.register(PublicBuilding)
|
@admin.register(PublicBuilding)
|
||||||
class PublicBuildingAdmin(admin.GISModelAdmin):
|
class PublicBuildingAdmin(admin.GISModelAdmin):
|
||||||
list_display = ("code", "name_fr", "location_type", "parent_location", "room_number", "level_number", "usage_type", "building_status", "is_heated", "is_erp", "dpe_rating")
|
list_display = ("code", "name_fr", "location_type", "parent_location", "usage_type", "building_status", "owner", "manager", "is_erp", "dpe_rating")
|
||||||
list_filter = ("location_type", "floor_type", "is_heated", "usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible")
|
list_filter = ("location_type", "usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible")
|
||||||
search_fields = ("code", "name_fr", "name_nl", "address", "owner", "room_number")
|
search_fields = ("code", "name_fr", "name_nl", "address", "owner")
|
||||||
raw_id_fields = ("parent_location", "manager")
|
raw_id_fields = ("parent_location", "manager")
|
||||||
inlines = [PublicBuildingDocumentInline]
|
|
||||||
|
|
||||||
@admin.register(BuildingAsset)
|
@admin.register(BuildingAsset)
|
||||||
class BuildingAssetAdmin(admin.GISModelAdmin):
|
class BuildingAssetAdmin(admin.GISModelAdmin):
|
||||||
list_display = ("code", "name_fr", "asset_type", "building", "location", "parent_asset", "material", "power_kw", "status", "obsolescence", "next_control_date")
|
list_display = ("code", "name_fr", "asset_type", "building", "location", "parent_asset", "status", "obsolescence")
|
||||||
list_filter = ("asset_type", "energy_source", "glazing_type", "opening_type", "status", "obsolescence")
|
list_filter = ("asset_type", "status", "obsolescence")
|
||||||
search_fields = ("code", "name_fr", "name_nl", "serial_number", "material", "maintenance_contract_ref", "contractor_name")
|
search_fields = ("code", "name_fr", "name_nl", "serial_number")
|
||||||
raw_id_fields = ("building", "location", "parent_asset")
|
raw_id_fields = ("building", "location", "parent_asset")
|
||||||
|
|
||||||
@admin.register(PublicBuildingDocument)
|
admin.site.register(PublicBuildingDocument, AssetAdmin)
|
||||||
class PublicBuildingDocumentAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("id", "building", "floor_location", "document_type", "file", "upload_date")
|
|
||||||
list_filter = ("document_type",)
|
|
||||||
search_fields = ("description", "building__code", "building__name_fr", "floor_location__code")
|
|
||||||
raw_id_fields = ("building", "floor_location")
|
|
||||||
|
|
||||||
@admin.register(OutdoorLocation)
|
@admin.register(OutdoorLocation)
|
||||||
class OutdoorLocationAdmin(admin.GISModelAdmin):
|
class OutdoorLocationAdmin(admin.GISModelAdmin):
|
||||||
|
|
@ -787,58 +733,3 @@ class ParkingSpotAdmin(AssetGISAdmin):
|
||||||
search_fields = ("code", "name_fr", "name_nl", "note")
|
search_fields = ("code", "name_fr", "name_nl", "note")
|
||||||
filter_horizontal = ("poles", "panels")
|
filter_horizontal = ("poles", "panels")
|
||||||
raw_id_fields = ("location", "street")
|
raw_id_fields = ("location", "street")
|
||||||
|
|
||||||
# ─── Management Zones ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
from django.utils.html import format_html
|
|
||||||
from django.contrib.contenttypes.admin import GenericTabularInline
|
|
||||||
from .models import ManagementZoneType, ManagementZone, ManagementZoneAsset, ManagementZoneDocument
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneDocumentInline(admin.TabularInline):
|
|
||||||
model = ManagementZoneDocument
|
|
||||||
extra = 1
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneAssetInline(GenericTabularInline):
|
|
||||||
model = ManagementZoneAsset
|
|
||||||
ct_field = 'content_type'
|
|
||||||
ct_fk_field = 'object_id'
|
|
||||||
extra = 1
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ManagementZoneType)
|
|
||||||
class ManagementZoneTypeAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('code', 'name_fr', 'name_nl', 'thematic', 'color_badge')
|
|
||||||
list_filter = ('thematic',)
|
|
||||||
search_fields = ('code', 'name_fr', 'name_nl', 'description')
|
|
||||||
|
|
||||||
def color_badge(self, obj):
|
|
||||||
color = obj.default_color or "#198754"
|
|
||||||
return format_html(
|
|
||||||
'<span style="display:inline-block;width:16px;height:16px;background-color:{};border:1px solid #999;border-radius:3px;vertical-align:middle;margin-right:6px;"></span>{}',
|
|
||||||
color, color
|
|
||||||
)
|
|
||||||
color_badge.short_description = "Couleur"
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ManagementZone)
|
|
||||||
class ManagementZoneAdmin(AssetGISAdmin):
|
|
||||||
list_display = ('code', 'name_fr', 'thematic', 'zone_type', 'color_badge', 'target_frequency_days', 'is_active', 'has_geom')
|
|
||||||
list_filter = ('thematic', 'zone_type', 'is_active')
|
|
||||||
search_fields = ('code', 'name_fr', 'name_nl', 'instructions', 'description', 'seasonality_notes')
|
|
||||||
readonly_fields = ('created_at', 'updated_at')
|
|
||||||
inlines = [ManagementZoneDocumentInline, ManagementZoneAssetInline]
|
|
||||||
|
|
||||||
def color_badge(self, obj):
|
|
||||||
color = obj.get_color()
|
|
||||||
return format_html(
|
|
||||||
'<span style="display:inline-block;width:16px;height:16px;background-color:{};border:1px solid #999;border-radius:3px;vertical-align:middle;margin-right:6px;"></span>{}',
|
|
||||||
color, color
|
|
||||||
)
|
|
||||||
color_badge.short_description = "Couleur"
|
|
||||||
|
|
||||||
def has_geom(self, obj):
|
|
||||||
return bool(obj.geom)
|
|
||||||
has_geom.boolean = True
|
|
||||||
has_geom.short_description = "Géométrie"
|
|
||||||
|
|
|
||||||
|
|
@ -54,15 +54,4 @@ class AssetsConfig(AppConfig):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
register("assets", "InspectionDocument", "file", guard_inspection_document)
|
register('assets', 'InspectionDocument', 'file', guard_inspection_document)
|
||||||
|
|
||||||
# Guards for PublicBuilding floor plans and documents
|
|
||||||
def guard_publicbuilding_floor_plan(obj, user):
|
|
||||||
return user.is_authenticated
|
|
||||||
|
|
||||||
register("assets", "PublicBuilding", "floor_plan", guard_publicbuilding_floor_plan)
|
|
||||||
|
|
||||||
def guard_publicbuilding_document(obj, user):
|
|
||||||
return user.is_authenticated
|
|
||||||
|
|
||||||
register("assets", "PublicBuildingDocument", "file", guard_publicbuilding_document)
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from assets.models import TrafficLightIntersectionDocument, PublicLightingStreetDocument, StructureDocument, ControlCenterDocument
|
from assets.models import TrafficLightIntersectionDocument, PublicLightingStreetDocument, StructureDocument, ControlCenterDocument
|
||||||
|
|
@ -288,7 +287,6 @@ class PublicBuildingForm(forms.ModelForm):
|
||||||
'usage_type', 'building_status',
|
'usage_type', 'building_status',
|
||||||
'construction_year', 'floors_count', 'basement_count',
|
'construction_year', 'floors_count', 'basement_count',
|
||||||
'total_surface_m2', 'usable_surface_m2', 'surface_m2', 'level_number',
|
'total_surface_m2', 'usable_surface_m2', 'surface_m2', 'level_number',
|
||||||
'room_number', 'ceiling_height_m', 'is_heated', 'fire_compartment', 'floor_type', 'floor_plan',
|
|
||||||
'is_erp', 'erp_category', 'erp_type', 'max_capacity', 'pmr_accessible',
|
'is_erp', 'erp_category', 'erp_type', 'max_capacity', 'pmr_accessible',
|
||||||
'dpe_rating', 'ges_rating', 'has_asbestos_diag', 'has_lead_diag',
|
'dpe_rating', 'ges_rating', 'has_asbestos_diag', 'has_lead_diag',
|
||||||
'lat', 'lon',
|
'lat', 'lon',
|
||||||
|
|
@ -314,12 +312,6 @@ class PublicBuildingForm(forms.ModelForm):
|
||||||
'usable_surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
'usable_surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
||||||
'surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
'surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
||||||
'level_number': forms.NumberInput(attrs={'class': 'form-control'}),
|
'level_number': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
'room_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: B-204, RDC-01'}),
|
|
||||||
'ceiling_height_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
|
||||||
'floor_plan': forms.FileInput(attrs={'class': 'form-control', 'accept': '.png,.jpg,.jpeg,.svg,.pdf,.webp'}),
|
|
||||||
'is_heated': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
||||||
'fire_compartment': forms.TextInput(attrs={'class': 'form-control'}),
|
|
||||||
'floor_type': forms.Select(attrs={'class': 'form-select'}),
|
|
||||||
'is_erp': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
'is_erp': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
'erp_category': forms.Select(attrs={'class': 'form-select'}),
|
'erp_category': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'erp_type': forms.Select(attrs={'class': 'form-select'}),
|
'erp_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
|
@ -335,15 +327,29 @@ class PublicBuildingForm(forms.ModelForm):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
for field_name in [
|
self.fields['name_fr'].required = False
|
||||||
'name_fr', 'name_nl', 'parent_location', 'location_type', 'address', 'description',
|
self.fields['name_nl'].required = False
|
||||||
'owner', 'manager', 'occupant_entity', 'construction_year', 'floors_count',
|
self.fields['parent_location'].required = False
|
||||||
'basement_count', 'total_surface_m2', 'usable_surface_m2', 'surface_m2', 'level_number',
|
self.fields['location_type'].required = False
|
||||||
'room_number', 'ceiling_height_m', 'fire_compartment', 'floor_type',
|
self.fields['address'].required = False
|
||||||
'erp_category', 'erp_type', 'max_capacity', 'dpe_rating', 'ges_rating', 'lat', 'lon'
|
self.fields['description'].required = False
|
||||||
]:
|
self.fields['owner'].required = False
|
||||||
if field_name in self.fields:
|
self.fields['manager'].required = False
|
||||||
self.fields[field_name].required = False
|
self.fields['occupant_entity'].required = False
|
||||||
|
self.fields['construction_year'].required = False
|
||||||
|
self.fields['floors_count'].required = False
|
||||||
|
self.fields['basement_count'].required = False
|
||||||
|
self.fields['total_surface_m2'].required = False
|
||||||
|
self.fields['usable_surface_m2'].required = False
|
||||||
|
self.fields['surface_m2'].required = False
|
||||||
|
self.fields['level_number'].required = False
|
||||||
|
self.fields['erp_category'].required = False
|
||||||
|
self.fields['erp_type'].required = False
|
||||||
|
self.fields['max_capacity'].required = False
|
||||||
|
self.fields['dpe_rating'].required = False
|
||||||
|
self.fields['ges_rating'].required = False
|
||||||
|
self.fields['lat'].required = False
|
||||||
|
self.fields['lon'].required = False
|
||||||
|
|
||||||
|
|
||||||
class BuildingAssetForm(forms.ModelForm):
|
class BuildingAssetForm(forms.ModelForm):
|
||||||
|
|
@ -352,14 +358,8 @@ class BuildingAssetForm(forms.ModelForm):
|
||||||
model = BuildingAsset
|
model = BuildingAsset
|
||||||
fields = [
|
fields = [
|
||||||
'code', 'name_fr', 'name_nl', 'building', 'location', 'parent_asset',
|
'code', 'name_fr', 'name_nl', 'building', 'location', 'parent_asset',
|
||||||
'asset_type', 'category', 'status', 'obsolescence',
|
'asset_type', 'category', 'status', 'obsolescence', 'installation_date',
|
||||||
'dimensions', 'surface_m2', 'material',
|
'serial_number', 'last_control_date', 'next_control_date', 'note'
|
||||||
'power_kw', 'energy_source', 'refrigerant_type', 'refrigerant_charge_kg',
|
|
||||||
'glazing_type', 'fire_rating', 'opening_type',
|
|
||||||
'installation_date', 'serial_number',
|
|
||||||
'maintenance_contract_ref', 'contractor_name',
|
|
||||||
'last_control_date', 'next_control_date', 'regulatory_periodicity_months',
|
|
||||||
'replacement_cost_estimate', 'note'
|
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
|
@ -372,37 +372,16 @@ class BuildingAssetForm(forms.ModelForm):
|
||||||
'category': forms.Select(attrs={'class': 'form-select'}),
|
'category': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'obsolescence': forms.Select(attrs={'class': 'form-select'}),
|
'obsolescence': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'dimensions': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: 120x140 cm, 350 m²'}),
|
|
||||||
'surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
|
||||||
'material': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: Aluminium, PVC, Zinc, Acier'}),
|
|
||||||
'power_kw': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
|
|
||||||
'energy_source': forms.Select(attrs={'class': 'form-select'}),
|
|
||||||
'refrigerant_type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: R32, R410A'}),
|
|
||||||
'refrigerant_charge_kg': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
|
||||||
'glazing_type': forms.Select(attrs={'class': 'form-select'}),
|
|
||||||
'fire_rating': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: EI30, EI60, Coupe-feu 1h'}),
|
|
||||||
'opening_type': forms.Select(attrs={'class': 'form-select'}),
|
|
||||||
'installation_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
'installation_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
||||||
'serial_number': forms.TextInput(attrs={'class': 'form-control'}),
|
'serial_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'maintenance_contract_ref': forms.TextInput(attrs={'class': 'form-control'}),
|
|
||||||
'contractor_name': forms.TextInput(attrs={'class': 'form-control'}),
|
|
||||||
'last_control_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
'last_control_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||||
'next_control_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
'next_control_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||||
'regulatory_periodicity_months': forms.NumberInput(attrs={'class': 'form-control'}),
|
|
||||||
'replacement_cost_estimate': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
|
||||||
'note': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
'note': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
for field_name in [
|
for field_name in ['name_fr', 'name_nl', 'location', 'parent_asset', 'category', 'installation_date', 'serial_number', 'last_control_date', 'next_control_date', 'note']:
|
||||||
'name_fr', 'name_nl', 'location', 'parent_asset', 'category',
|
|
||||||
'dimensions', 'surface_m2', 'material', 'power_kw', 'energy_source',
|
|
||||||
'refrigerant_type', 'refrigerant_charge_kg', 'glazing_type', 'fire_rating', 'opening_type',
|
|
||||||
'installation_date', 'serial_number', 'maintenance_contract_ref', 'contractor_name',
|
|
||||||
'last_control_date', 'next_control_date', 'regulatory_periodicity_months',
|
|
||||||
'replacement_cost_estimate', 'note'
|
|
||||||
]:
|
|
||||||
if field_name in self.fields:
|
if field_name in self.fields:
|
||||||
self.fields[field_name].required = False
|
self.fields[field_name].required = False
|
||||||
from common.models import Thematic
|
from common.models import Thematic
|
||||||
|
|
@ -461,98 +440,3 @@ class StructureExceptionalTransportForm(forms.ModelForm):
|
||||||
for name, field in self.fields.items():
|
for name, field in self.fields.items():
|
||||||
if not isinstance(field.widget, forms.CheckboxInput):
|
if not isinstance(field.widget, forms.CheckboxInput):
|
||||||
field.required = False
|
field.required = False
|
||||||
|
|
||||||
# ─── Management Zones Forms ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
from assets.models import ManagementZone, ManagementZoneType, ManagementZoneDocument
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneForm(forms.ModelForm):
|
|
||||||
"""Formulaire de création et d'édition d'une Zone de gestion."""
|
|
||||||
geom_geojson = forms.CharField(required=False, widget=forms.HiddenInput())
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ManagementZone
|
|
||||||
fields = [
|
|
||||||
'code', 'name_fr', 'name_nl', 'thematic', 'zone_type', 'color',
|
|
||||||
'target_frequency_days', 'seasonality_notes', 'instructions', 'description', 'is_active'
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
|
||||||
'name_fr': forms.TextInput(attrs={'class': 'form-control'}),
|
|
||||||
'name_nl': forms.TextInput(attrs={'class': 'form-control'}),
|
|
||||||
'thematic': forms.Select(attrs={'class': 'form-select'}),
|
|
||||||
'zone_type': forms.Select(attrs={'class': 'form-select'}),
|
|
||||||
'color': forms.TextInput(attrs={'class': 'form-control form-control-color', 'type': 'color'}),
|
|
||||||
'target_frequency_days': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
|
|
||||||
'seasonality_notes': forms.TextInput(attrs={'class': 'form-control', 'placeholder': "Ex: Tonte d'avril à octobre; fauche tardive en automne"}),
|
|
||||||
'instructions': forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': "Consignes d'exploitation, précautions tramway, biodiversité..."}),
|
|
||||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
||||||
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, user=None, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for field in ['name_nl', 'zone_type', 'color', 'target_frequency_days', 'seasonality_notes', 'instructions', 'description']:
|
|
||||||
if field in self.fields:
|
|
||||||
self.fields[field].required = False
|
|
||||||
|
|
||||||
if not self.instance.pk and not self.initial.get('color'):
|
|
||||||
self.initial['color'] = '#198754'
|
|
||||||
|
|
||||||
if self.instance and self.instance.pk and self.instance.geom:
|
|
||||||
geom_wgs84 = self.instance.geom.transform(4326, clone=True)
|
|
||||||
self.initial['geom_geojson'] = geom_wgs84.geojson
|
|
||||||
|
|
||||||
if user:
|
|
||||||
from assets.permissions import get_editable_thematics_for_management_zones
|
|
||||||
editable_thematics = get_editable_thematics_for_management_zones(user)
|
|
||||||
self.fields['thematic'].queryset = editable_thematics
|
|
||||||
if not self.instance.pk and editable_thematics.count() == 1:
|
|
||||||
self.fields['thematic'].initial = editable_thematics.first()
|
|
||||||
|
|
||||||
thematic_id = None
|
|
||||||
if self.data.get('thematic'):
|
|
||||||
thematic_id = self.data.get('thematic')
|
|
||||||
elif self.instance.pk and self.instance.thematic_id:
|
|
||||||
thematic_id = self.instance.thematic_id
|
|
||||||
elif self.initial.get('thematic'):
|
|
||||||
thematic_id = self.initial.get('thematic')
|
|
||||||
if hasattr(thematic_id, 'pk'):
|
|
||||||
thematic_id = thematic_id.pk
|
|
||||||
|
|
||||||
if thematic_id:
|
|
||||||
self.fields['zone_type'].queryset = ManagementZoneType.objects.filter(thematic_id=thematic_id)
|
|
||||||
else:
|
|
||||||
self.fields['zone_type'].queryset = ManagementZoneType.objects.all()
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
zone = super().save(commit=False)
|
|
||||||
geojson_str = (self.cleaned_data.get('geom_geojson') or '').strip()
|
|
||||||
if geojson_str:
|
|
||||||
try:
|
|
||||||
geom = GEOSGeometry(geojson_str, srid=4326)
|
|
||||||
if isinstance(geom, Polygon):
|
|
||||||
geom = MultiPolygon(geom, srid=4326)
|
|
||||||
geom.transform(3812)
|
|
||||||
zone.geom = geom
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
elif 'geom_geojson' in self.cleaned_data and not geojson_str:
|
|
||||||
zone.geom = None
|
|
||||||
|
|
||||||
if commit:
|
|
||||||
zone.save()
|
|
||||||
return zone
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneDocumentForm(forms.ModelForm):
|
|
||||||
documents = MultipleFileField(required=True, label="Documents")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ManagementZoneDocument
|
|
||||||
fields = ['documents']
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -4833,8 +4833,8 @@ msgid "Description / Remarques"
|
||||||
msgstr "Description / Remarks"
|
msgstr "Description / Remarks"
|
||||||
|
|
||||||
#: assets/templates/assets/components/asset_interventions.html:169
|
#: assets/templates/assets/components/asset_interventions.html:169
|
||||||
msgid "Détails ou remarques globales..."
|
msgid "Détails ou remarques initiales..."
|
||||||
msgstr "Details or global remarks..."
|
msgstr "Details or initial remarks..."
|
||||||
|
|
||||||
#: assets/templates/assets/components/asset_interventions.html:176
|
#: assets/templates/assets/components/asset_interventions.html:176
|
||||||
#: assets/templates/assets/components/location_edit_modals.html:98
|
#: assets/templates/assets/components/location_edit_modals.html:98
|
||||||
|
|
@ -4966,7 +4966,7 @@ msgstr "Remarks / Observations"
|
||||||
|
|
||||||
#: assets/templates/assets/components/inspection_form_modal.html:129
|
#: assets/templates/assets/components/inspection_form_modal.html:129
|
||||||
msgid "Saisir vos remarques ou détails..."
|
msgid "Saisir vos remarques ou détails..."
|
||||||
msgstr "Details or global remarks..."
|
msgstr "Details or initial remarks..."
|
||||||
|
|
||||||
#: assets/templates/assets/components/inspection_form_modal.html:138
|
#: assets/templates/assets/components/inspection_form_modal.html:138
|
||||||
msgid "Valider l'inspection"
|
msgid "Valider l'inspection"
|
||||||
|
|
@ -8168,7 +8168,7 @@ msgstr "Accessory"
|
||||||
|
|
||||||
#: assets/views/core.py:447
|
#: assets/views/core.py:447
|
||||||
msgid "Surface verte"
|
msgid "Surface verte"
|
||||||
msgstr "Green surface"
|
msgstr "Slippery surface"
|
||||||
|
|
||||||
#: assets/views/core.py:449
|
#: assets/views/core.py:449
|
||||||
msgid "Arbuste"
|
msgid "Arbuste"
|
||||||
|
|
@ -8573,126 +8573,6 @@ msgstr "GIEP zones"
|
||||||
msgid "Aucune localisation GIEP trouvée à proximité"
|
msgid "Aucune localisation GIEP trouvée à proximité"
|
||||||
msgstr "No location found"
|
msgstr "No location found"
|
||||||
|
|
||||||
msgid "Supprimer de la DB"
|
|
||||||
msgstr "Delete from DB"
|
|
||||||
|
|
||||||
msgid "Supprimer l'existant de la DB"
|
|
||||||
msgstr "Delete existing from DB"
|
|
||||||
|
|
||||||
msgid "Ne pas créer (Ignorer)"
|
|
||||||
msgstr "Do not create (Ignore)"
|
|
||||||
|
|
||||||
msgid "Archiver l'existant (Ne pas recréer)"
|
|
||||||
msgstr "Archive existing (do not recreate)"
|
|
||||||
|
|
||||||
msgid "Archiver l'existant"
|
|
||||||
msgstr "Archive existing"
|
|
||||||
|
|
||||||
msgid "Aide au tracé des câbles"
|
|
||||||
msgstr "Cable tracing help"
|
|
||||||
|
|
||||||
msgid "Tracer"
|
|
||||||
msgstr "Trace"
|
|
||||||
|
|
||||||
msgid "Double-clic sur la carte pour ajouter un point."
|
|
||||||
msgstr "Double-click on the map to add a point."
|
|
||||||
|
|
||||||
msgid "Ajouter un nœud"
|
|
||||||
msgstr "Add a node"
|
|
||||||
|
|
||||||
msgid "Double-clic sur un segment du câble."
|
|
||||||
msgstr "Double-click on a cable segment."
|
|
||||||
|
|
||||||
msgid "Supprimer un nœud"
|
|
||||||
msgstr "Delete a node"
|
|
||||||
|
|
||||||
msgid "Double-clic sur un nœud (poignée orange)."
|
|
||||||
msgstr "Double-click on a node (orange handle)."
|
|
||||||
|
|
||||||
msgid "Déplacer"
|
|
||||||
msgstr "Move"
|
|
||||||
|
|
||||||
msgid "Glisser-déposer les poignées pour ajuster."
|
|
||||||
msgstr "Drag and drop handles to adjust."
|
|
||||||
|
|
||||||
msgid "Copier tracé"
|
|
||||||
msgstr "Copy trace"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)."
|
|
||||||
msgstr ""
|
|
||||||
"Copy the trace of another cable following the same path (e.g. 48 and tel.)."
|
|
||||||
|
|
||||||
msgid "Copier tracé…"
|
|
||||||
msgstr "Copy trace…"
|
|
||||||
|
|
||||||
msgid "Copier le tracé d'un autre câble"
|
|
||||||
msgstr "Copy another cable's trace"
|
|
||||||
|
|
||||||
msgid "Appliquer aux autres câbles…"
|
|
||||||
msgstr "Apply to other cables…"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud "
|
|
||||||
"pour le supprimer • Glisser pour déplacer"
|
|
||||||
msgstr ""
|
|
||||||
"Double-click a segment to add a node • Double-click a node to delete • Drag "
|
|
||||||
"to move"
|
|
||||||
|
|
||||||
msgid "Appliquer le tracé à d'autres câbles"
|
|
||||||
msgstr "Apply trace to other cables"
|
|
||||||
|
|
||||||
msgid "Copier le tracé de"
|
|
||||||
msgstr "Copy trace from"
|
|
||||||
|
|
||||||
msgid "vers les câbles sélectionnés ci-dessous :"
|
|
||||||
msgstr "to the selected cables below:"
|
|
||||||
|
|
||||||
msgid "Copier le tracé"
|
|
||||||
msgstr "Copy trace"
|
|
||||||
|
|
||||||
msgid "Le câble source ne possède aucun tracé valide à copier."
|
|
||||||
msgstr "The source cable has no valid trace to copy."
|
|
||||||
|
|
||||||
msgid "Aucun tracé disponible"
|
|
||||||
msgstr "No trace available"
|
|
||||||
|
|
||||||
msgid "Tracé copié de"
|
|
||||||
msgstr "Trace copied from"
|
|
||||||
|
|
||||||
msgid "vers"
|
|
||||||
msgstr "to"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Veuillez d'abord tracer au moins deux points pour ce câble avant de le "
|
|
||||||
"dupliquer."
|
|
||||||
msgstr "Please trace at least two points for this cable before duplicating."
|
|
||||||
|
|
||||||
msgid "Aucun autre câble disponible dans ce carrefour."
|
|
||||||
msgstr "No other cable available at this intersection."
|
|
||||||
|
|
||||||
msgid "Veuillez sélectionner au moins un câble cible."
|
|
||||||
msgstr "Please select at least one target cable."
|
|
||||||
|
|
||||||
msgid "câble(s) mis à jour avec ce tracé !"
|
|
||||||
msgstr "cable(s) updated with this trace!"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Tout replier"
|
|
||||||
msgstr "Collapse all"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Tout déplier"
|
|
||||||
msgstr "Expand all"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Assets physiques rattachés"
|
|
||||||
msgstr "Linked physical assets"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Assets rattachés directement au bâtiment"
|
|
||||||
msgstr "Assets directly linked to building"
|
|
||||||
|
|
||||||
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
||||||
#~ msgstr "Missing from Excel file (Will be kept or archived)"
|
#~ msgstr "Missing from Excel file (Will be kept or archived)"
|
||||||
|
|
||||||
|
|
@ -8710,57 +8590,3 @@ msgstr "Assets directly linked to building"
|
||||||
|
|
||||||
#~ msgid "Arceaux cargo constatés"
|
#~ msgid "Arceaux cargo constatés"
|
||||||
#~ msgstr "Observed cargo bike racks"
|
#~ msgstr "Observed cargo bike racks"
|
||||||
|
|
||||||
msgid "Type de zone de gestion"
|
|
||||||
msgstr "Management zone type"
|
|
||||||
|
|
||||||
msgid "Types de zones de gestion"
|
|
||||||
msgstr "Management zone types"
|
|
||||||
|
|
||||||
msgid "Zone de gestion"
|
|
||||||
msgstr "Management zone"
|
|
||||||
|
|
||||||
msgid "Zones de gestion"
|
|
||||||
msgstr "Management zones"
|
|
||||||
|
|
||||||
msgid "Consignes et points d'attention"
|
|
||||||
msgstr "Instructions and points of attention"
|
|
||||||
|
|
||||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
|
||||||
msgstr "Operational instructions, points of vigilance, safety rules..."
|
|
||||||
|
|
||||||
msgid "Périmètre géographique"
|
|
||||||
msgstr "Geographical perimeter"
|
|
||||||
|
|
||||||
msgid "Couleur d'affichage"
|
|
||||||
msgstr "Display color"
|
|
||||||
|
|
||||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
|
||||||
msgstr "Hexadecimal code (e.g. #198754). If empty, the zone type color is used."
|
|
||||||
|
|
||||||
msgid "Fréquence cible (jours)"
|
|
||||||
msgstr "Target frequency (days)"
|
|
||||||
|
|
||||||
msgid "Intervalle indicatif entre interventions en jours."
|
|
||||||
msgstr "Indicative interval between interventions in days."
|
|
||||||
|
|
||||||
msgid "Saisonnalité / Calendrier"
|
|
||||||
msgstr "Seasonality / Calendar"
|
|
||||||
|
|
||||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
|
||||||
msgstr "E.g.: Mowing from April to October; late mowing in September; pruning prohibited in spring."
|
|
||||||
|
|
||||||
msgid "Asset de zone de gestion"
|
|
||||||
msgstr "Management zone asset"
|
|
||||||
|
|
||||||
msgid "Assets de zones de gestion"
|
|
||||||
msgstr "Management zone assets"
|
|
||||||
|
|
||||||
msgid "Document de zone de gestion"
|
|
||||||
msgstr "Management zone document"
|
|
||||||
|
|
||||||
msgid "Documents de zones de gestion"
|
|
||||||
msgstr "Management zone documents"
|
|
||||||
|
|
||||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
|
||||||
msgstr "If checked, this asset is excluded from the zone even if its geometry intersects the perimeter."
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -613,134 +613,3 @@ msgstr "Collapse models"
|
||||||
#: loko/assets/static/assets/unified_index.js:3726
|
#: loko/assets/static/assets/unified_index.js:3726
|
||||||
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:93
|
|
||||||
msgid "Rechercher une adresse…"
|
|
||||||
msgstr "Search an address…"
|
|
||||||
|
|
||||||
msgid "Rechercher une adresse..."
|
|
||||||
msgstr "Search an address..."
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:748
|
|
||||||
msgid "Géolocalisation..."
|
|
||||||
msgstr "Geolocation..."
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:769
|
|
||||||
msgid "Position actualisée"
|
|
||||||
msgstr "Position updated"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:773
|
|
||||||
msgid "GPS indisponible"
|
|
||||||
msgstr "GPS unavailable"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:935
|
|
||||||
msgid "Inspections (Désactivé)"
|
|
||||||
msgstr "Inspections (Disabled)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:936
|
|
||||||
msgid "Inspections (1 mois)"
|
|
||||||
msgstr "Inspections (1 month)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:937
|
|
||||||
msgid "Inspections (3 mois)"
|
|
||||||
msgstr "Inspections (3 months)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:938
|
|
||||||
msgid "Inspections (6 mois)"
|
|
||||||
msgstr "Inspections (6 months)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:939
|
|
||||||
msgid "Inspections (1 an)"
|
|
||||||
msgstr "Inspections (1 year)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:940
|
|
||||||
msgid "Inspections (Tout)"
|
|
||||||
msgstr "Inspections (All)"
|
|
||||||
|
|
||||||
msgid "Panneau"
|
|
||||||
msgstr "Sign"
|
|
||||||
|
|
||||||
msgid "Supprimer ce panneau"
|
|
||||||
msgstr "Delete this sign"
|
|
||||||
|
|
||||||
msgid "Mention (ex: Sauf riverains)"
|
|
||||||
msgstr "Mention (e.g. Except local traffic)"
|
|
||||||
|
|
||||||
msgid "Propreté :"
|
|
||||||
msgstr "Cleanliness:"
|
|
||||||
|
|
||||||
msgid "Propre"
|
|
||||||
msgstr "Clean"
|
|
||||||
|
|
||||||
msgid "Sale"
|
|
||||||
msgstr "Dirty"
|
|
||||||
|
|
||||||
msgid "Végétation"
|
|
||||||
msgstr "Vegetation"
|
|
||||||
|
|
||||||
msgid "Dégradé"
|
|
||||||
msgstr "Damaged"
|
|
||||||
|
|
||||||
msgid "Coordonnées GPS manquantes."
|
|
||||||
msgstr "Missing GPS coordinates."
|
|
||||||
|
|
||||||
msgid "Veuillez ajouter au moins un panneau."
|
|
||||||
msgstr "Please add at least one sign."
|
|
||||||
|
|
||||||
msgid "Panneaux créés avec succès !"
|
|
||||||
msgstr "Signs created successfully!"
|
|
||||||
|
|
||||||
msgid "Erreur : "
|
|
||||||
msgstr "Error: "
|
|
||||||
|
|
||||||
msgid "Impossible de créer les panneaux."
|
|
||||||
msgstr "Unable to create signs."
|
|
||||||
|
|
||||||
msgid "Erreur lors de la création : "
|
|
||||||
msgstr "Error during creation: "
|
|
||||||
|
|
||||||
msgid "Créer et Enregistrer"
|
|
||||||
msgstr "Create and Save"
|
|
||||||
|
|
||||||
msgid "Inspection enregistrée !"
|
|
||||||
msgstr "Inspection saved!"
|
|
||||||
|
|
||||||
msgid "Intervention"
|
|
||||||
msgstr "Intervention"
|
|
||||||
|
|
||||||
msgid "créée."
|
|
||||||
msgstr "created."
|
|
||||||
|
|
||||||
msgid "Erreur lors de la validation."
|
|
||||||
msgstr "Validation error."
|
|
||||||
|
|
||||||
msgid "Erreur réseau."
|
|
||||||
msgstr "Network error."
|
|
||||||
|
|
||||||
|
|
||||||
msgid "Zones de gestion"
|
|
||||||
msgstr "Management zones"
|
|
||||||
|
|
||||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
|
||||||
msgstr "No management zone defined for this thematic."
|
|
||||||
|
|
||||||
msgid "zones"
|
|
||||||
msgstr "zones"
|
|
||||||
|
|
||||||
msgid "Afficher / masquer toutes les zones"
|
|
||||||
msgstr "Show / hide all zones"
|
|
||||||
|
|
||||||
msgid "Opacité"
|
|
||||||
msgstr "Opacity"
|
|
||||||
|
|
||||||
msgid "Fréquence cible :"
|
|
||||||
msgstr "Target frequency:"
|
|
||||||
|
|
||||||
msgid "jours"
|
|
||||||
msgstr "days"
|
|
||||||
|
|
||||||
msgid "Consignes & points d'attention"
|
|
||||||
msgstr "Instructions & points of attention"
|
|
||||||
|
|
||||||
msgid "Centrer sur la zone"
|
|
||||||
msgstr "Center on zone"
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -4839,8 +4839,8 @@ msgid "Description / Remarques"
|
||||||
msgstr "Description / Remarques"
|
msgstr "Description / Remarques"
|
||||||
|
|
||||||
#: assets/templates/assets/components/asset_interventions.html:169
|
#: assets/templates/assets/components/asset_interventions.html:169
|
||||||
msgid "Détails ou remarques globales..."
|
msgid "Détails ou remarques initiales..."
|
||||||
msgstr "Détails ou remarques globales..."
|
msgstr "Détails ou remarques initiales..."
|
||||||
|
|
||||||
#: assets/templates/assets/components/asset_interventions.html:176
|
#: assets/templates/assets/components/asset_interventions.html:176
|
||||||
#: assets/templates/assets/components/location_edit_modals.html:98
|
#: assets/templates/assets/components/location_edit_modals.html:98
|
||||||
|
|
@ -4972,7 +4972,7 @@ msgstr "Remarques / Observations"
|
||||||
|
|
||||||
#: assets/templates/assets/components/inspection_form_modal.html:129
|
#: assets/templates/assets/components/inspection_form_modal.html:129
|
||||||
msgid "Saisir vos remarques ou détails..."
|
msgid "Saisir vos remarques ou détails..."
|
||||||
msgstr "Détails ou remarques globales..."
|
msgstr "Détails ou remarques initiales..."
|
||||||
|
|
||||||
#: assets/templates/assets/components/inspection_form_modal.html:138
|
#: assets/templates/assets/components/inspection_form_modal.html:138
|
||||||
msgid "Valider l'inspection"
|
msgid "Valider l'inspection"
|
||||||
|
|
@ -8624,128 +8624,6 @@ msgstr "Zones GIEP"
|
||||||
msgid "Aucune localisation GIEP trouvée à proximité"
|
msgid "Aucune localisation GIEP trouvée à proximité"
|
||||||
msgstr "Aucune localisation GIEP trouvée à proximité"
|
msgstr "Aucune localisation GIEP trouvée à proximité"
|
||||||
|
|
||||||
msgid "Supprimer de la DB"
|
|
||||||
msgstr "Supprimer de la DB"
|
|
||||||
|
|
||||||
msgid "Supprimer l'existant de la DB"
|
|
||||||
msgstr "Supprimer l'existant de la DB"
|
|
||||||
|
|
||||||
msgid "Ne pas créer (Ignorer)"
|
|
||||||
msgstr "Ne pas créer (Ignorer)"
|
|
||||||
|
|
||||||
msgid "Archiver l'existant (Ne pas recréer)"
|
|
||||||
msgstr "Archiver l'existant (Ne pas recréer)"
|
|
||||||
|
|
||||||
msgid "Archiver l'existant"
|
|
||||||
msgstr "Archiver l'existant"
|
|
||||||
|
|
||||||
msgid "Aide au tracé des câbles"
|
|
||||||
msgstr "Aide au tracé des câbles"
|
|
||||||
|
|
||||||
msgid "Tracer"
|
|
||||||
msgstr "Tracer"
|
|
||||||
|
|
||||||
msgid "Double-clic sur la carte pour ajouter un point."
|
|
||||||
msgstr "Double-clic sur la carte pour ajouter un point."
|
|
||||||
|
|
||||||
msgid "Ajouter un nœud"
|
|
||||||
msgstr "Ajouter un nœud"
|
|
||||||
|
|
||||||
msgid "Double-clic sur un segment du câble."
|
|
||||||
msgstr "Double-clic sur un segment du câble."
|
|
||||||
|
|
||||||
msgid "Supprimer un nœud"
|
|
||||||
msgstr "Supprimer un nœud"
|
|
||||||
|
|
||||||
msgid "Double-clic sur un nœud (poignée orange)."
|
|
||||||
msgstr "Double-clic sur un nœud (poignée orange)."
|
|
||||||
|
|
||||||
msgid "Déplacer"
|
|
||||||
msgstr "Déplacer"
|
|
||||||
|
|
||||||
msgid "Glisser-déposer les poignées pour ajuster."
|
|
||||||
msgstr "Glisser-déposer les poignées pour ajuster."
|
|
||||||
|
|
||||||
msgid "Copier tracé"
|
|
||||||
msgstr "Copier tracé"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)."
|
|
||||||
msgstr ""
|
|
||||||
"Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)."
|
|
||||||
|
|
||||||
msgid "Copier tracé…"
|
|
||||||
msgstr "Copier tracé…"
|
|
||||||
|
|
||||||
msgid "Copier le tracé d'un autre câble"
|
|
||||||
msgstr "Copier le tracé d'un autre câble"
|
|
||||||
|
|
||||||
msgid "Appliquer aux autres câbles…"
|
|
||||||
msgstr "Appliquer aux autres câbles…"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud "
|
|
||||||
"pour le supprimer • Glisser pour déplacer"
|
|
||||||
msgstr ""
|
|
||||||
"Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud "
|
|
||||||
"pour le supprimer • Glisser pour déplacer"
|
|
||||||
|
|
||||||
msgid "Appliquer le tracé à d'autres câbles"
|
|
||||||
msgstr "Appliquer le tracé à d'autres câbles"
|
|
||||||
|
|
||||||
msgid "Copier le tracé de"
|
|
||||||
msgstr "Copier le tracé de"
|
|
||||||
|
|
||||||
msgid "vers les câbles sélectionnés ci-dessous :"
|
|
||||||
msgstr "vers les câbles sélectionnés ci-dessous :"
|
|
||||||
|
|
||||||
msgid "Copier le tracé"
|
|
||||||
msgstr "Copier le tracé"
|
|
||||||
|
|
||||||
msgid "Le câble source ne possède aucun tracé valide à copier."
|
|
||||||
msgstr "Le câble source ne possède aucun tracé valide à copier."
|
|
||||||
|
|
||||||
msgid "Aucun tracé disponible"
|
|
||||||
msgstr "Aucun tracé disponible"
|
|
||||||
|
|
||||||
msgid "Tracé copié de"
|
|
||||||
msgstr "Tracé copié de"
|
|
||||||
|
|
||||||
msgid "vers"
|
|
||||||
msgstr "vers"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Veuillez d'abord tracer au moins deux points pour ce câble avant de le "
|
|
||||||
"dupliquer."
|
|
||||||
msgstr ""
|
|
||||||
"Veuillez d'abord tracer au moins deux points pour ce câble avant de le "
|
|
||||||
"dupliquer."
|
|
||||||
|
|
||||||
msgid "Aucun autre câble disponible dans ce carrefour."
|
|
||||||
msgstr "Aucun autre câble disponible dans ce carrefour."
|
|
||||||
|
|
||||||
msgid "Veuillez sélectionner au moins un câble cible."
|
|
||||||
msgstr "Veuillez sélectionner au moins un câble cible."
|
|
||||||
|
|
||||||
msgid "câble(s) mis à jour avec ce tracé !"
|
|
||||||
msgstr "câble(s) mis à jour avec ce tracé !"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Tout replier"
|
|
||||||
msgstr "Tout replier"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Tout déplier"
|
|
||||||
msgstr "Tout déplier"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Assets physiques rattachés"
|
|
||||||
msgstr "Assets physiques rattachés"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Assets rattachés directement au bâtiment"
|
|
||||||
msgstr "Assets rattachés directement au bâtiment"
|
|
||||||
|
|
||||||
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
||||||
#~ msgstr "Absent du fichier Excel (Sera conservé ou archivé)"
|
#~ msgstr "Absent du fichier Excel (Sera conservé ou archivé)"
|
||||||
|
|
||||||
|
|
@ -8763,57 +8641,3 @@ msgstr "Assets rattachés directement au bâtiment"
|
||||||
|
|
||||||
#~ msgid "Arceaux cargo constatés"
|
#~ msgid "Arceaux cargo constatés"
|
||||||
#~ msgstr "Arceaux cargo constatés"
|
#~ msgstr "Arceaux cargo constatés"
|
||||||
|
|
||||||
msgid "Type de zone de gestion"
|
|
||||||
msgstr "Type de zone de gestion"
|
|
||||||
|
|
||||||
msgid "Types de zones de gestion"
|
|
||||||
msgstr "Types de zones de gestion"
|
|
||||||
|
|
||||||
msgid "Zone de gestion"
|
|
||||||
msgstr "Zone de gestion"
|
|
||||||
|
|
||||||
msgid "Zones de gestion"
|
|
||||||
msgstr "Zones de gestion"
|
|
||||||
|
|
||||||
msgid "Consignes et points d'attention"
|
|
||||||
msgstr "Consignes et points d'attention"
|
|
||||||
|
|
||||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
|
||||||
msgstr "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
|
||||||
|
|
||||||
msgid "Périmètre géographique"
|
|
||||||
msgstr "Périmètre géographique"
|
|
||||||
|
|
||||||
msgid "Couleur d'affichage"
|
|
||||||
msgstr "Couleur d'affichage"
|
|
||||||
|
|
||||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
|
||||||
msgstr "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
|
||||||
|
|
||||||
msgid "Fréquence cible (jours)"
|
|
||||||
msgstr "Fréquence cible (jours)"
|
|
||||||
|
|
||||||
msgid "Intervalle indicatif entre interventions en jours."
|
|
||||||
msgstr "Intervalle indicatif entre interventions en jours."
|
|
||||||
|
|
||||||
msgid "Saisonnalité / Calendrier"
|
|
||||||
msgstr "Saisonnalité / Calendrier"
|
|
||||||
|
|
||||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
|
||||||
msgstr "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
|
||||||
|
|
||||||
msgid "Asset de zone de gestion"
|
|
||||||
msgstr "Asset de zone de gestion"
|
|
||||||
|
|
||||||
msgid "Assets de zones de gestion"
|
|
||||||
msgstr "Assets de zones de gestion"
|
|
||||||
|
|
||||||
msgid "Document de zone de gestion"
|
|
||||||
msgstr "Document de zone de gestion"
|
|
||||||
|
|
||||||
msgid "Documents de zones de gestion"
|
|
||||||
msgstr "Documents de zones de gestion"
|
|
||||||
|
|
||||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
|
||||||
msgstr "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -618,134 +618,3 @@ msgstr "Replier les modèles"
|
||||||
#: loko/assets/static/assets/unified_index.js:3726
|
#: loko/assets/static/assets/unified_index.js:3726
|
||||||
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
||||||
msgstr "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
msgstr "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:93
|
|
||||||
msgid "Rechercher une adresse…"
|
|
||||||
msgstr "Rechercher une adresse…"
|
|
||||||
|
|
||||||
msgid "Rechercher une adresse..."
|
|
||||||
msgstr "Rechercher une adresse..."
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:748
|
|
||||||
msgid "Géolocalisation..."
|
|
||||||
msgstr "Géolocalisation..."
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:769
|
|
||||||
msgid "Position actualisée"
|
|
||||||
msgstr "Position actualisée"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:773
|
|
||||||
msgid "GPS indisponible"
|
|
||||||
msgstr "GPS indisponible"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:935
|
|
||||||
msgid "Inspections (Désactivé)"
|
|
||||||
msgstr "Inspections (Désactivé)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:936
|
|
||||||
msgid "Inspections (1 mois)"
|
|
||||||
msgstr "Inspections (1 mois)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:937
|
|
||||||
msgid "Inspections (3 mois)"
|
|
||||||
msgstr "Inspections (3 mois)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:938
|
|
||||||
msgid "Inspections (6 mois)"
|
|
||||||
msgstr "Inspections (6 mois)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:939
|
|
||||||
msgid "Inspections (1 an)"
|
|
||||||
msgstr "Inspections (1 an)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:940
|
|
||||||
msgid "Inspections (Tout)"
|
|
||||||
msgstr "Inspections (Tout)"
|
|
||||||
|
|
||||||
msgid "Panneau"
|
|
||||||
msgstr "Panneau"
|
|
||||||
|
|
||||||
msgid "Supprimer ce panneau"
|
|
||||||
msgstr "Supprimer ce panneau"
|
|
||||||
|
|
||||||
msgid "Mention (ex: Sauf riverains)"
|
|
||||||
msgstr "Mention (ex: Sauf riverains)"
|
|
||||||
|
|
||||||
msgid "Propreté :"
|
|
||||||
msgstr "Propreté :"
|
|
||||||
|
|
||||||
msgid "Propre"
|
|
||||||
msgstr "Propre"
|
|
||||||
|
|
||||||
msgid "Sale"
|
|
||||||
msgstr "Sale"
|
|
||||||
|
|
||||||
msgid "Végétation"
|
|
||||||
msgstr "Végétation"
|
|
||||||
|
|
||||||
msgid "Dégradé"
|
|
||||||
msgstr "Dégradé"
|
|
||||||
|
|
||||||
msgid "Coordonnées GPS manquantes."
|
|
||||||
msgstr "Coordonnées GPS manquantes."
|
|
||||||
|
|
||||||
msgid "Veuillez ajouter au moins un panneau."
|
|
||||||
msgstr "Veuillez ajouter au moins un panneau."
|
|
||||||
|
|
||||||
msgid "Panneaux créés avec succès !"
|
|
||||||
msgstr "Panneaux créés avec succès !"
|
|
||||||
|
|
||||||
msgid "Erreur : "
|
|
||||||
msgstr "Erreur : "
|
|
||||||
|
|
||||||
msgid "Impossible de créer les panneaux."
|
|
||||||
msgstr "Impossible de créer les panneaux."
|
|
||||||
|
|
||||||
msgid "Erreur lors de la création : "
|
|
||||||
msgstr "Erreur lors de la création : "
|
|
||||||
|
|
||||||
msgid "Créer et Enregistrer"
|
|
||||||
msgstr "Créer et Enregistrer"
|
|
||||||
|
|
||||||
msgid "Inspection enregistrée !"
|
|
||||||
msgstr "Inspection enregistrée !"
|
|
||||||
|
|
||||||
msgid "Intervention"
|
|
||||||
msgstr "Intervention"
|
|
||||||
|
|
||||||
msgid "créée."
|
|
||||||
msgstr "créée."
|
|
||||||
|
|
||||||
msgid "Erreur lors de la validation."
|
|
||||||
msgstr "Erreur lors de la validation."
|
|
||||||
|
|
||||||
msgid "Erreur réseau."
|
|
||||||
msgstr "Erreur réseau."
|
|
||||||
|
|
||||||
|
|
||||||
msgid "Zones de gestion"
|
|
||||||
msgstr "Zones de gestion"
|
|
||||||
|
|
||||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
|
||||||
msgstr "Aucune zone de gestion définie pour cette thématique."
|
|
||||||
|
|
||||||
msgid "zones"
|
|
||||||
msgstr "zones"
|
|
||||||
|
|
||||||
msgid "Afficher / masquer toutes les zones"
|
|
||||||
msgstr "Afficher / masquer toutes les zones"
|
|
||||||
|
|
||||||
msgid "Opacité"
|
|
||||||
msgstr "Opacité"
|
|
||||||
|
|
||||||
msgid "Fréquence cible :"
|
|
||||||
msgstr "Fréquence cible :"
|
|
||||||
|
|
||||||
msgid "jours"
|
|
||||||
msgstr "jours"
|
|
||||||
|
|
||||||
msgid "Consignes & points d'attention"
|
|
||||||
msgstr "Consignes & points d'attention"
|
|
||||||
|
|
||||||
msgid "Centrer sur la zone"
|
|
||||||
msgstr "Centrer sur la zone"
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -4834,8 +4834,8 @@ msgid "Description / Remarques"
|
||||||
msgstr "Hardwarebeschrijving"
|
msgstr "Hardwarebeschrijving"
|
||||||
|
|
||||||
#: assets/templates/assets/components/asset_interventions.html:169
|
#: assets/templates/assets/components/asset_interventions.html:169
|
||||||
msgid "Détails ou remarques globales..."
|
msgid "Détails ou remarques initiales..."
|
||||||
msgstr "Details of globale opmerkingen..."
|
msgstr "Details of eerste opmerkingen..."
|
||||||
|
|
||||||
#: assets/templates/assets/components/asset_interventions.html:176
|
#: assets/templates/assets/components/asset_interventions.html:176
|
||||||
#: assets/templates/assets/components/location_edit_modals.html:98
|
#: assets/templates/assets/components/location_edit_modals.html:98
|
||||||
|
|
@ -4967,7 +4967,7 @@ msgstr "Observaties"
|
||||||
|
|
||||||
#: assets/templates/assets/components/inspection_form_modal.html:129
|
#: assets/templates/assets/components/inspection_form_modal.html:129
|
||||||
msgid "Saisir vos remarques ou détails..."
|
msgid "Saisir vos remarques ou détails..."
|
||||||
msgstr "Details of globale opmerkingen..."
|
msgstr "Details of eerste opmerkingen..."
|
||||||
|
|
||||||
#: assets/templates/assets/components/inspection_form_modal.html:138
|
#: assets/templates/assets/components/inspection_form_modal.html:138
|
||||||
msgid "Valider l'inspection"
|
msgid "Valider l'inspection"
|
||||||
|
|
@ -8572,128 +8572,6 @@ msgstr "GIEP-zones"
|
||||||
msgid "Aucune localisation GIEP trouvée à proximité"
|
msgid "Aucune localisation GIEP trouvée à proximité"
|
||||||
msgstr "Geen locatie gevonden."
|
msgstr "Geen locatie gevonden."
|
||||||
|
|
||||||
msgid "Supprimer de la DB"
|
|
||||||
msgstr "Verwijderen uit de database"
|
|
||||||
|
|
||||||
msgid "Supprimer l'existant de la DB"
|
|
||||||
msgstr "Bestaande verwijderen uit de database"
|
|
||||||
|
|
||||||
msgid "Ne pas créer (Ignorer)"
|
|
||||||
msgstr "Niet aanmaken (Negeren)"
|
|
||||||
|
|
||||||
msgid "Archiver l'existant (Ne pas recréer)"
|
|
||||||
msgstr "Bestaande archiveren (niet opnieuw aanmaken)"
|
|
||||||
|
|
||||||
msgid "Archiver l'existant"
|
|
||||||
msgstr "Bestaande archiveren"
|
|
||||||
|
|
||||||
msgid "Aide au tracé des câbles"
|
|
||||||
msgstr "Hulp bij het traceren van kabels"
|
|
||||||
|
|
||||||
msgid "Tracer"
|
|
||||||
msgstr "Traceren"
|
|
||||||
|
|
||||||
msgid "Double-clic sur la carte pour ajouter un point."
|
|
||||||
msgstr "Dubbelklik op de kaart om een punt toe te voegen."
|
|
||||||
|
|
||||||
msgid "Ajouter un nœud"
|
|
||||||
msgstr "Knooppunt toevoegen"
|
|
||||||
|
|
||||||
msgid "Double-clic sur un segment du câble."
|
|
||||||
msgstr "Dubbelklik op een segment van de kabel."
|
|
||||||
|
|
||||||
msgid "Supprimer un nœud"
|
|
||||||
msgstr "Knooppunt verwijderen"
|
|
||||||
|
|
||||||
msgid "Double-clic sur un nœud (poignée orange)."
|
|
||||||
msgstr "Dubbelklik op een knooppunt (oranje handgreep)."
|
|
||||||
|
|
||||||
msgid "Déplacer"
|
|
||||||
msgstr "Verplaatsen"
|
|
||||||
|
|
||||||
msgid "Glisser-déposer les poignées pour ajuster."
|
|
||||||
msgstr "Sleep de handgrepen om aan te passen."
|
|
||||||
|
|
||||||
msgid "Copier tracé"
|
|
||||||
msgstr "Tracé kopiëren"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)."
|
|
||||||
msgstr ""
|
|
||||||
"Kopieer het tracé van een andere kabel die hetzelfde traject volgt (bv. 48 "
|
|
||||||
"en tel.)."
|
|
||||||
|
|
||||||
msgid "Copier tracé…"
|
|
||||||
msgstr "Tracé kopiëren…"
|
|
||||||
|
|
||||||
msgid "Copier le tracé d'un autre câble"
|
|
||||||
msgstr "Het tracé van een andere kabel kopiëren"
|
|
||||||
|
|
||||||
msgid "Appliquer aux autres câbles…"
|
|
||||||
msgstr "Toepassen op andere kabels…"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud "
|
|
||||||
"pour le supprimer • Glisser pour déplacer"
|
|
||||||
msgstr ""
|
|
||||||
"Dubbelklik op een segment om een knooppunt toe te voegen • Dubbelklik op een"
|
|
||||||
" knooppunt om te verwijderen • Sleep om te verplaatsen"
|
|
||||||
|
|
||||||
msgid "Appliquer le tracé à d'autres câbles"
|
|
||||||
msgstr "Tracé toepassen op andere kabels"
|
|
||||||
|
|
||||||
msgid "Copier le tracé de"
|
|
||||||
msgstr "Tracé kopiëren van"
|
|
||||||
|
|
||||||
msgid "vers les câbles sélectionnés ci-dessous :"
|
|
||||||
msgstr "naar de onderstaande geselecteerde kabels:"
|
|
||||||
|
|
||||||
msgid "Copier le tracé"
|
|
||||||
msgstr "Tracé kopiëren"
|
|
||||||
|
|
||||||
msgid "Le câble source ne possède aucun tracé valide à copier."
|
|
||||||
msgstr "De bronkabel heeft geen geldig tracé om te kopiëren."
|
|
||||||
|
|
||||||
msgid "Aucun tracé disponible"
|
|
||||||
msgstr "Geen tracé beschikbaar"
|
|
||||||
|
|
||||||
msgid "Tracé copié de"
|
|
||||||
msgstr "Tracé gekopieerd van"
|
|
||||||
|
|
||||||
msgid "vers"
|
|
||||||
msgstr "naar"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Veuillez d'abord tracer au moins deux points pour ce câble avant de le "
|
|
||||||
"dupliquer."
|
|
||||||
msgstr ""
|
|
||||||
"Teken eerst ten minste twee punten voor deze kabel alvorens te dupliceren."
|
|
||||||
|
|
||||||
msgid "Aucun autre câble disponible dans ce carrefour."
|
|
||||||
msgstr "Geen andere kabel beschikbaar op dit kruispunt."
|
|
||||||
|
|
||||||
msgid "Veuillez sélectionner au moins un câble cible."
|
|
||||||
msgstr "Selecteer ten minste één doelkabel."
|
|
||||||
|
|
||||||
msgid "câble(s) mis à jour avec ce tracé !"
|
|
||||||
msgstr "kabel(s) bijgewerkt met dit tracé!"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Tout replier"
|
|
||||||
msgstr "Alles inklappen"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Tout déplier"
|
|
||||||
msgstr "Alles uitklappen"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Assets physiques rattachés"
|
|
||||||
msgstr "Gekoppelde fysieke assets"
|
|
||||||
|
|
||||||
#: assets/templates/assets/buildings/publicbuilding_detail.html
|
|
||||||
msgid "Assets rattachés directement au bâtiment"
|
|
||||||
msgstr "Assets direct gekoppeld aan gebouw"
|
|
||||||
|
|
||||||
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
||||||
#~ msgstr "Ontbreekt in het Excel-bestand (Wordt bewaard of gearchiveerd)"
|
#~ msgstr "Ontbreekt in het Excel-bestand (Wordt bewaard of gearchiveerd)"
|
||||||
|
|
||||||
|
|
@ -8711,57 +8589,3 @@ msgstr "Assets direct gekoppeld aan gebouw"
|
||||||
|
|
||||||
#~ msgid "Arceaux cargo constatés"
|
#~ msgid "Arceaux cargo constatés"
|
||||||
#~ msgstr "Vastgestelde cargobeugels"
|
#~ msgstr "Vastgestelde cargobeugels"
|
||||||
|
|
||||||
msgid "Type de zone de gestion"
|
|
||||||
msgstr "Beheerszonetype"
|
|
||||||
|
|
||||||
msgid "Types de zones de gestion"
|
|
||||||
msgstr "Beheerszonetypes"
|
|
||||||
|
|
||||||
msgid "Zone de gestion"
|
|
||||||
msgstr "Beheerszone"
|
|
||||||
|
|
||||||
msgid "Zones de gestion"
|
|
||||||
msgstr "Beheerszones"
|
|
||||||
|
|
||||||
msgid "Consignes et points d'attention"
|
|
||||||
msgstr "Instructies en aandachtspunten"
|
|
||||||
|
|
||||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
|
||||||
msgstr "Exploitatie-instructies, aandachtspunten, veiligheidsregels..."
|
|
||||||
|
|
||||||
msgid "Périmètre géographique"
|
|
||||||
msgstr "Geografische perimeter"
|
|
||||||
|
|
||||||
msgid "Couleur d'affichage"
|
|
||||||
msgstr "Weergavekleur"
|
|
||||||
|
|
||||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
|
||||||
msgstr "Hexadecimale code (bijv. #198754). Indien leeg, wordt de kleur van het zonetype gebruikt."
|
|
||||||
|
|
||||||
msgid "Fréquence cible (jours)"
|
|
||||||
msgstr "Doelfrequentie (dagen)"
|
|
||||||
|
|
||||||
msgid "Intervalle indicatif entre interventions en jours."
|
|
||||||
msgstr "Indicatief interval tussen interventies in dagen."
|
|
||||||
|
|
||||||
msgid "Saisonnalité / Calendrier"
|
|
||||||
msgstr "Seizoensgebondenheid / Kalender"
|
|
||||||
|
|
||||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
|
||||||
msgstr "Bijv.: Maaien van april tot oktober; laat maaien in september; snoeiverbod in het voorjaar."
|
|
||||||
|
|
||||||
msgid "Asset de zone de gestion"
|
|
||||||
msgstr "Beheerszone-asset"
|
|
||||||
|
|
||||||
msgid "Assets de zones de gestion"
|
|
||||||
msgstr "Beheerszone-assets"
|
|
||||||
|
|
||||||
msgid "Document de zone de gestion"
|
|
||||||
msgstr "Beheerszonedocument"
|
|
||||||
|
|
||||||
msgid "Documents de zones de gestion"
|
|
||||||
msgstr "Beheerszonedocumenten"
|
|
||||||
|
|
||||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
|
||||||
msgstr "Indien aangevinkt, wordt dit asset uitgesloten van de zone, zelfs als de geometrie de perimeter doorsnijdt."
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -615,134 +615,3 @@ msgstr "Modellen inklappen"
|
||||||
#: loko/assets/static/assets/unified_index.js:3726
|
#: loko/assets/static/assets/unified_index.js:3726
|
||||||
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:93
|
|
||||||
msgid "Rechercher une adresse…"
|
|
||||||
msgstr "Zoek een adres…"
|
|
||||||
|
|
||||||
msgid "Rechercher une adresse..."
|
|
||||||
msgstr "Zoek een adres..."
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:748
|
|
||||||
msgid "Géolocalisation..."
|
|
||||||
msgstr "Geolocatie..."
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:769
|
|
||||||
msgid "Position actualisée"
|
|
||||||
msgstr "Positie bijgewerkt"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:773
|
|
||||||
msgid "GPS indisponible"
|
|
||||||
msgstr "GPS niet beschikbaar"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:935
|
|
||||||
msgid "Inspections (Désactivé)"
|
|
||||||
msgstr "Inspecties (Uitgeschakeld)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:936
|
|
||||||
msgid "Inspections (1 mois)"
|
|
||||||
msgstr "Inspecties (1 maand)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:937
|
|
||||||
msgid "Inspections (3 mois)"
|
|
||||||
msgstr "Inspecties (3 maanden)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:938
|
|
||||||
msgid "Inspections (6 mois)"
|
|
||||||
msgstr "Inspecties (6 maanden)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:939
|
|
||||||
msgid "Inspections (1 an)"
|
|
||||||
msgstr "Inspecties (1 jaar)"
|
|
||||||
|
|
||||||
#: loko/assets/static/assets/inspection_map.js:940
|
|
||||||
msgid "Inspections (Tout)"
|
|
||||||
msgstr "Inspecties (Alles)"
|
|
||||||
|
|
||||||
msgid "Panneau"
|
|
||||||
msgstr "Bord"
|
|
||||||
|
|
||||||
msgid "Supprimer ce panneau"
|
|
||||||
msgstr "Dit bord verwijderen"
|
|
||||||
|
|
||||||
msgid "Mention (ex: Sauf riverains)"
|
|
||||||
msgstr "Vermelding (bijv. Uitgezonderd aangelanden)"
|
|
||||||
|
|
||||||
msgid "Propreté :"
|
|
||||||
msgstr "Netheid:"
|
|
||||||
|
|
||||||
msgid "Propre"
|
|
||||||
msgstr "Schoon"
|
|
||||||
|
|
||||||
msgid "Sale"
|
|
||||||
msgstr "Vuil"
|
|
||||||
|
|
||||||
msgid "Végétation"
|
|
||||||
msgstr "Vegetatie"
|
|
||||||
|
|
||||||
msgid "Dégradé"
|
|
||||||
msgstr "Beschadigd"
|
|
||||||
|
|
||||||
msgid "Coordonnées GPS manquantes."
|
|
||||||
msgstr "Ontbrekende gps-coördinaten."
|
|
||||||
|
|
||||||
msgid "Veuillez ajouter au moins un panneau."
|
|
||||||
msgstr "Voeg minimaal één bord toe."
|
|
||||||
|
|
||||||
msgid "Panneaux créés avec succès !"
|
|
||||||
msgstr "Borden succesvol aangemaakt!"
|
|
||||||
|
|
||||||
msgid "Erreur : "
|
|
||||||
msgstr "Fout: "
|
|
||||||
|
|
||||||
msgid "Impossible de créer les panneaux."
|
|
||||||
msgstr "Kan de borden niet aanmaken."
|
|
||||||
|
|
||||||
msgid "Erreur lors de la création : "
|
|
||||||
msgstr "Fout bij aanmaken: "
|
|
||||||
|
|
||||||
msgid "Créer et Enregistrer"
|
|
||||||
msgstr "Aanmaken en opslaan"
|
|
||||||
|
|
||||||
msgid "Inspection enregistrée !"
|
|
||||||
msgstr "Inspectie opgeslagen!"
|
|
||||||
|
|
||||||
msgid "Intervention"
|
|
||||||
msgstr "Interventie"
|
|
||||||
|
|
||||||
msgid "créée."
|
|
||||||
msgstr "aangemaakt."
|
|
||||||
|
|
||||||
msgid "Erreur lors de la validation."
|
|
||||||
msgstr "Fout bij validatie."
|
|
||||||
|
|
||||||
msgid "Erreur réseau."
|
|
||||||
msgstr "Netwerkfout."
|
|
||||||
|
|
||||||
|
|
||||||
msgid "Zones de gestion"
|
|
||||||
msgstr "Beheerszones"
|
|
||||||
|
|
||||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
|
||||||
msgstr "Geen beheerszone gedefinieerd voor dit thema."
|
|
||||||
|
|
||||||
msgid "zones"
|
|
||||||
msgstr "zones"
|
|
||||||
|
|
||||||
msgid "Afficher / masquer toutes les zones"
|
|
||||||
msgstr "Alle zones tonen / verbergen"
|
|
||||||
|
|
||||||
msgid "Opacité"
|
|
||||||
msgstr "Dekking"
|
|
||||||
|
|
||||||
msgid "Fréquence cible :"
|
|
||||||
msgstr "Doelfrequentie:"
|
|
||||||
|
|
||||||
msgid "jours"
|
|
||||||
msgstr "dagen"
|
|
||||||
|
|
||||||
msgid "Consignes & points d'attention"
|
|
||||||
msgstr "Instructies & aandachtspunten"
|
|
||||||
|
|
||||||
msgid "Centrer sur la zone"
|
|
||||||
msgstr "Centreren op zone"
|
|
||||||
|
|
|
||||||
|
|
@ -1,220 +0,0 @@
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.db import connection, transaction
|
|
||||||
from assets.models import NatureLocation, NatureTree
|
|
||||||
from common.models import Municipality
|
|
||||||
|
|
||||||
|
|
||||||
MUNICIPALITY_PREFIXES = {
|
|
||||||
'AND': 'Anderlecht',
|
|
||||||
'AUD': 'Auderghem',
|
|
||||||
'BRK': 'Berchem-Sainte-Agathe',
|
|
||||||
'BSA': 'Berchem-Sainte-Agathe',
|
|
||||||
'BXL': 'Bruxelles',
|
|
||||||
'BRU': 'Bruxelles',
|
|
||||||
'ETT': 'Etterbeek',
|
|
||||||
'EVE': 'Evere',
|
|
||||||
'FOR': 'Forest',
|
|
||||||
'GAN': 'Ganshoren',
|
|
||||||
'IXL': 'Ixelles',
|
|
||||||
'IXE': 'Ixelles',
|
|
||||||
'JET': 'Jette',
|
|
||||||
'KOE': 'Koekelberg',
|
|
||||||
'MOL': 'Molenbeek-Saint-Jean',
|
|
||||||
'STG': 'Saint-Gilles',
|
|
||||||
'STJ': 'Saint-Josse-ten-Noode',
|
|
||||||
'SCH': 'Schaerbeek',
|
|
||||||
'UCC': 'Uccle',
|
|
||||||
'WAT': 'Watermael-Boitsfort',
|
|
||||||
'WLU': 'Woluwe-Saint-Lambert',
|
|
||||||
'WSL': 'Woluwe-Saint-Lambert',
|
|
||||||
'WSP': 'Woluwe-Saint-Pierre',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Rattache spatialement les arbres (NatureTree) aux tronçons des sections vertes (NatureLocation, ex: WAT_R22_008)."
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument(
|
|
||||||
'--max-distance',
|
|
||||||
type=float,
|
|
||||||
default=25.0,
|
|
||||||
help="Distance maximale en mètres pour le rattachement (défaut: 25.0m, 0 pour intersection stricte)"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--section-code',
|
|
||||||
type=str,
|
|
||||||
default=None,
|
|
||||||
help="Code d'un tronçon spécifique à cibler (ex: WAT_R22_008)"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--dry-run',
|
|
||||||
action='store_true',
|
|
||||||
help="Simuler le rattachement sans modifier la base de données"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--skip-sync-munis',
|
|
||||||
action='store_true',
|
|
||||||
help="Ne pas synchroniser la commune sur les sections vertes"
|
|
||||||
)
|
|
||||||
|
|
||||||
def log(self, msg):
|
|
||||||
self.stdout.write(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}")
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
max_dist = options['max_distance']
|
|
||||||
target_section = options['section_code']
|
|
||||||
dry_run = options['dry_run']
|
|
||||||
sync_munis = not options['skip_sync_munis']
|
|
||||||
|
|
||||||
self.log("=== RATTACHEMENT SPATIAL DES ARBRES AUX TRONÇONS DE SECTIONS VERTES ===")
|
|
||||||
self.log(f"Distance max : {max_dist} m {'(Intersection stricte)' if max_dist == 0 else ''}")
|
|
||||||
if target_section:
|
|
||||||
self.log(f"Section ciblée : {target_section}")
|
|
||||||
if dry_run:
|
|
||||||
self.log("[SIMULATION / DRY-RUN] Aucune modification ne sera enregistrée.")
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
# 1. Synchronisation des communes sur les sections vertes
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
if sync_munis and not dry_run:
|
|
||||||
self.log("1. Synchronisation des communes sur les sections vertes...")
|
|
||||||
munis_by_name = {m.name_fr.lower(): m for m in Municipality.objects.all()}
|
|
||||||
updated_munis = 0
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
# Intersections géométriques
|
|
||||||
cursor.execute("""
|
|
||||||
UPDATE assets_naturelocation l
|
|
||||||
SET municipality_id = m.id
|
|
||||||
FROM common_municipality m
|
|
||||||
WHERE l.code ~ '^[A-Z]{3}_'
|
|
||||||
AND l.municipality_id IS NULL
|
|
||||||
AND l.geom IS NOT NULL
|
|
||||||
AND ST_Intersects(ST_PointOnSurface(l.geom), m.geom);
|
|
||||||
""")
|
|
||||||
updated_munis += cursor.rowcount
|
|
||||||
|
|
||||||
# Complément par préfixe de code pour les cas frontaliers
|
|
||||||
locs_without_muni = NatureLocation.objects.filter(code__regex=r'^[A-Z]{3}_', municipality__isnull=True)
|
|
||||||
for loc in locs_without_muni:
|
|
||||||
pfx = loc.code[:3].upper()
|
|
||||||
muni_name = MUNICIPALITY_PREFIXES.get(pfx)
|
|
||||||
if muni_name and muni_name.lower() in munis_by_name:
|
|
||||||
loc.municipality = munis_by_name[muni_name.lower()]
|
|
||||||
loc.save(update_fields=['municipality'])
|
|
||||||
updated_munis += 1
|
|
||||||
|
|
||||||
self.log(f" Communes renseignées sur {updated_munis} sections vertes.")
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
# 2. Analyse et Rattachement des Arbres
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
t0 = time.time()
|
|
||||||
self.log("2. Analyse géospatiale du rattachement arbres -> sections vertes...")
|
|
||||||
|
|
||||||
section_filter_sql = ""
|
|
||||||
params = [max_dist]
|
|
||||||
if target_section:
|
|
||||||
section_filter_sql = "AND nearest.code = %s"
|
|
||||||
params.append(target_section)
|
|
||||||
|
|
||||||
count_query = f"""
|
|
||||||
WITH nearest AS (
|
|
||||||
SELECT
|
|
||||||
t.id AS tree_id,
|
|
||||||
nearest_loc.id AS loc_id,
|
|
||||||
nearest_loc.code AS loc_code,
|
|
||||||
nearest_loc.name_fr AS loc_name,
|
|
||||||
nearest_loc.dist
|
|
||||||
FROM assets_naturetree t
|
|
||||||
CROSS JOIN LATERAL (
|
|
||||||
SELECT l.id, l.code, l.name_fr, ST_Distance(t.geom, l.geom) AS dist
|
|
||||||
FROM assets_naturelocation l
|
|
||||||
WHERE l.code ~ '^[A-Z]{{3}}_' AND l.geom IS NOT NULL
|
|
||||||
ORDER BY t.geom <-> l.geom
|
|
||||||
LIMIT 1
|
|
||||||
) nearest_loc
|
|
||||||
WHERE t.geom IS NOT NULL AND t.lat != 0 AND t.lon != 0
|
|
||||||
AND nearest_loc.dist <= %s
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
COUNT(*) AS total_matched,
|
|
||||||
COUNT(*) FILTER (WHERE dist = 0) AS inside,
|
|
||||||
COUNT(*) FILTER (WHERE dist > 0 AND dist <= 5) AS within_5m,
|
|
||||||
COUNT(*) FILTER (WHERE dist > 5 AND dist <= 15) AS within_15m,
|
|
||||||
COUNT(*) FILTER (WHERE dist > 15 AND dist <= 25) AS within_25m,
|
|
||||||
COUNT(*) FILTER (WHERE dist > 25) AS beyond_25m,
|
|
||||||
COUNT(DISTINCT loc_id) AS distinct_sections
|
|
||||||
FROM nearest
|
|
||||||
WHERE 1=1 {section_filter_sql};
|
|
||||||
"""
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
cursor.execute(count_query, params)
|
|
||||||
stats = cursor.fetchone()
|
|
||||||
|
|
||||||
total_matched = stats[0]
|
|
||||||
inside = stats[1]
|
|
||||||
w5m = stats[2]
|
|
||||||
w15m = stats[3]
|
|
||||||
w25m = stats[4]
|
|
||||||
b25m = stats[5]
|
|
||||||
distinct_sections = stats[6]
|
|
||||||
|
|
||||||
self.log(f" Arbres éligibles au rattachement : {total_matched}")
|
|
||||||
self.log(f" - Directement à l'intérieur (0m) : {inside}")
|
|
||||||
self.log(f" - Proximité immédiate (1 à 5m) : {w5m}")
|
|
||||||
self.log(f" - Bordure de voirie (5 à 15m) : {w15m}")
|
|
||||||
self.log(f" - Zone étendue (15 à 25m) : {w25m}")
|
|
||||||
if b25m:
|
|
||||||
self.log(f" - Au-delà (> 25m) : {b25m}")
|
|
||||||
self.log(f" Nombre de tronçons / sections concernés : {distinct_sections}")
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
self.log("Simulation terminée avec succès (aucune écriture en base).")
|
|
||||||
return
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
# 3. Application de la mise à jour
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
self.log("3. Application de la mise à jour des liaisons (location_id)...")
|
|
||||||
update_query = f"""
|
|
||||||
WITH candidates AS (
|
|
||||||
SELECT
|
|
||||||
t.id AS tree_id,
|
|
||||||
nearest.id AS loc_id
|
|
||||||
FROM assets_naturetree t
|
|
||||||
CROSS JOIN LATERAL (
|
|
||||||
SELECT l.id, l.code, ST_Distance(t.geom, l.geom) AS dist
|
|
||||||
FROM assets_naturelocation l
|
|
||||||
WHERE l.code ~ '^[A-Z]{{3}}_' AND l.geom IS NOT NULL
|
|
||||||
ORDER BY t.geom <-> l.geom
|
|
||||||
LIMIT 1
|
|
||||||
) nearest
|
|
||||||
WHERE t.geom IS NOT NULL AND t.lat != 0 AND t.lon != 0
|
|
||||||
AND nearest.dist <= %s
|
|
||||||
{section_filter_sql}
|
|
||||||
)
|
|
||||||
UPDATE assets_naturetree t
|
|
||||||
SET location_id = c.loc_id
|
|
||||||
FROM candidates c
|
|
||||||
WHERE t.id = c.tree_id
|
|
||||||
AND (t.location_id IS DISTINCT FROM c.loc_id);
|
|
||||||
"""
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
cursor.execute(update_query, params)
|
|
||||||
updated_count = cursor.rowcount
|
|
||||||
|
|
||||||
t1 = time.time()
|
|
||||||
self.log(f"Mise à jour terminée : {updated_count} arbres rattachés en {t1 - t0:.2f} secondes.")
|
|
||||||
|
|
||||||
# Vérification ciblée (par exemple WAT_R22_008)
|
|
||||||
check_code = target_section or 'WAT_R22_008'
|
|
||||||
check_loc = NatureLocation.objects.filter(code=check_code).first()
|
|
||||||
if check_loc:
|
|
||||||
attached_cnt = check_loc.nature_trees.count()
|
|
||||||
self.log(f"Vérification sur {check_code} ({check_loc.name_fr}) : {attached_cnt} arbres rattachés.")
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
from datetime import timedelta
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.db import connection, transaction
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
|
|
||||||
from assets.models import NatureTree
|
|
||||||
from interventions.models import Intervention, SourceCategory
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = (
|
|
||||||
"Annule les interventions arboricoles caduques ou périmées :\n"
|
|
||||||
"1. Interventions prescrites avant un diagnostic plus récent sur l'arbre (remplacées par ré-inspection).\n"
|
|
||||||
"2. Prescriptions actives dont la date limite est échue depuis plus de X mois (défaut : 12 mois).\n"
|
|
||||||
"Met à jour les événements de timeline et recalcule automatiquement les dates de prochaine inspection."
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument(
|
|
||||||
'--months',
|
|
||||||
type=int,
|
|
||||||
default=12,
|
|
||||||
help="Délai en mois après échéance au-delà duquel une prescription non exécutée est annulée (défaut : 12).",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--dry-run',
|
|
||||||
action='store_true',
|
|
||||||
help="Simuler l'opération et afficher les comptages sans modifier la base de données.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--no-recalc-trees',
|
|
||||||
action='store_true',
|
|
||||||
help="Ne pas recalculer next_inspection_date sur les arbres après l'annulation.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
months = options['months']
|
|
||||||
dry_run = options['dry_run']
|
|
||||||
recalc_trees = not options['no_recalc_trees']
|
|
||||||
|
|
||||||
now = timezone.now()
|
|
||||||
# Calcul de la date seuil d'échéance (ex: il y a 12 mois)
|
|
||||||
cutoff_date = (now - timedelta(days=int(months * 30.4375))).date()
|
|
||||||
|
|
||||||
self.stdout.write(self.style.MIGRATE_HEADING(
|
|
||||||
f"=== Nettoyage des interventions d'arbres caduques et échues ==="
|
|
||||||
))
|
|
||||||
self.stdout.write(f"Date de référence : {now.strftime('%d/%m/%Y %H:%M')}")
|
|
||||||
self.stdout.write(f"Seuil d'échéance ({months} mois) : interventions échues avant le {cutoff_date.strftime('%d/%m/%Y')}\n")
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
self.stdout.write(self.style.WARNING("[MODE SIMULATION] Aucune modification ne sera enregistrée.\n"))
|
|
||||||
|
|
||||||
content_type_tree = ContentType.objects.get_for_model(NatureTree)
|
|
||||||
source_fulcrum = SourceCategory.objects.filter(name_fr="Fulcrum").first()
|
|
||||||
source_fulcrum_id = source_fulcrum.id if source_fulcrum else None
|
|
||||||
|
|
||||||
with connection.cursor() as cur:
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
# COMPTAGE RÈGLE 1 : Diagnostic plus récent sur l'arbre
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
cur.execute("""
|
|
||||||
SELECT COUNT(DISTINCT i.id)
|
|
||||||
FROM interventions_intervention i
|
|
||||||
JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
|
||||||
JOIN assets_naturetree t ON t.id = ia.object_id
|
|
||||||
WHERE ia.content_type_id = %s
|
|
||||||
AND i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND t.last_phytosanitary_date IS NOT NULL
|
|
||||||
AND (
|
|
||||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
|
||||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < t.last_phytosanitary_date)
|
|
||||||
);
|
|
||||||
""", [content_type_tree.id])
|
|
||||||
count_rule_diag = cur.fetchone()[0]
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
# COMPTAGE RÈGLE 2 : Échéance dépassée > X mois (sans être dans règle 1)
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
cur.execute("""
|
|
||||||
SELECT COUNT(DISTINCT i.id)
|
|
||||||
FROM interventions_intervention i
|
|
||||||
LEFT JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
|
||||||
LEFT JOIN assets_naturetree t ON t.id = ia.object_id AND ia.content_type_id = %s
|
|
||||||
WHERE i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND (ia.content_type_id = %s OR i.source_category_id = %s)
|
|
||||||
AND i.expected_end_time IS NOT NULL
|
|
||||||
AND i.expected_end_time::date < %s
|
|
||||||
AND NOT (
|
|
||||||
t.last_phytosanitary_date IS NOT NULL
|
|
||||||
AND (
|
|
||||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
|
||||||
OR (i.expected_end_time::date < t.last_phytosanitary_date)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
""", [content_type_tree.id, content_type_tree.id, source_fulcrum_id, cutoff_date])
|
|
||||||
count_rule_expired = cur.fetchone()[0]
|
|
||||||
|
|
||||||
# Total combiné
|
|
||||||
cur.execute("""
|
|
||||||
SELECT COUNT(DISTINCT i.id)
|
|
||||||
FROM interventions_intervention i
|
|
||||||
LEFT JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
|
||||||
LEFT JOIN assets_naturetree t ON t.id = ia.object_id AND ia.content_type_id = %s
|
|
||||||
WHERE i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND (ia.content_type_id = %s OR i.source_category_id = %s)
|
|
||||||
AND (
|
|
||||||
(t.last_phytosanitary_date IS NOT NULL AND (
|
|
||||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
|
||||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < t.last_phytosanitary_date)
|
|
||||||
))
|
|
||||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < %s)
|
|
||||||
);
|
|
||||||
""", [content_type_tree.id, content_type_tree.id, source_fulcrum_id, cutoff_date])
|
|
||||||
total_to_cancel = cur.fetchone()[0]
|
|
||||||
|
|
||||||
self.stdout.write(f"1. Prescriptions caduques (arbre ré-inspecté ultérieurement) : {count_rule_diag}")
|
|
||||||
self.stdout.write(f"2. Prescriptions échues depuis plus de {months} mois sans nouveau diag : {count_rule_expired}")
|
|
||||||
self.stdout.write(self.style.MIGRATE_LABEL(f"Total des interventions à annuler : {total_to_cancel}\n"))
|
|
||||||
|
|
||||||
if total_to_cancel == 0:
|
|
||||||
self.stdout.write(self.style.SUCCESS("Aucune intervention à annuler. La base est déjà assainie."))
|
|
||||||
return
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
self.stdout.write(self.style.WARNING(
|
|
||||||
f"[MODE SIMULATION] {total_to_cancel} interventions auraient été passées au statut 'canceled'.\n"
|
|
||||||
"Exécutez sans --dry-run pour appliquer."
|
|
||||||
))
|
|
||||||
return
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
# EXÉCUTION RÉELLE EN TRANSACTION
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
self.stdout.write("Application des annulations...")
|
|
||||||
with transaction.atomic():
|
|
||||||
with connection.cursor() as cur:
|
|
||||||
# 1a. Timeline events pour la règle 1 (diagnostic plus récent)
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO interventions_interventiontimeline (
|
|
||||||
intervention_id, event_time, event_type, from_status, to_status,
|
|
||||||
event_description, field_changes
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
i.id,
|
|
||||||
%s,
|
|
||||||
'status_change',
|
|
||||||
i.status,
|
|
||||||
'canceled',
|
|
||||||
'Intervention annulée automatiquement : prescription caduque suite au nouveau diagnostic de l''arbre du ' ||
|
|
||||||
TO_CHAR(t.last_phytosanitary_date, 'DD/MM/YYYY') || '.',
|
|
||||||
('{"status": ["' || i.status || '", "canceled"]}')::json
|
|
||||||
FROM interventions_intervention i
|
|
||||||
JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
|
||||||
JOIN assets_naturetree t ON t.id = ia.object_id
|
|
||||||
WHERE ia.content_type_id = %s
|
|
||||||
AND i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND t.last_phytosanitary_date IS NOT NULL
|
|
||||||
AND (
|
|
||||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
|
||||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < t.last_phytosanitary_date)
|
|
||||||
)
|
|
||||||
AND NOT EXISTS (
|
|
||||||
SELECT 1 FROM interventions_interventiontimeline tl
|
|
||||||
WHERE tl.intervention_id = i.id
|
|
||||||
AND tl.to_status = 'canceled'
|
|
||||||
);
|
|
||||||
""", [now, content_type_tree.id])
|
|
||||||
tl_diag_count = cur.rowcount
|
|
||||||
|
|
||||||
# 1b. Timeline events pour la règle 2 (échues > X mois)
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO interventions_interventiontimeline (
|
|
||||||
intervention_id, event_time, event_type, from_status, to_status,
|
|
||||||
event_description, field_changes
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
i.id,
|
|
||||||
%s,
|
|
||||||
'status_change',
|
|
||||||
i.status,
|
|
||||||
'canceled',
|
|
||||||
'Intervention annulée automatiquement : prescription échue depuis plus de ' || %s || ' mois (' ||
|
|
||||||
TO_CHAR(i.expected_end_time, 'DD/MM/YYYY') || ') sans nouveau diagnostic.',
|
|
||||||
('{"status": ["' || i.status || '", "canceled"]}')::json
|
|
||||||
FROM interventions_intervention i
|
|
||||||
LEFT JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
|
||||||
WHERE i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND (ia.content_type_id = %s OR i.source_category_id = %s)
|
|
||||||
AND i.expected_end_time IS NOT NULL
|
|
||||||
AND i.expected_end_time::date < %s
|
|
||||||
AND NOT EXISTS (
|
|
||||||
SELECT 1 FROM interventions_interventiontimeline tl
|
|
||||||
WHERE tl.intervention_id = i.id
|
|
||||||
AND tl.to_status = 'canceled'
|
|
||||||
);
|
|
||||||
""", [now, months, content_type_tree.id, source_fulcrum_id, cutoff_date])
|
|
||||||
tl_expired_count = cur.rowcount
|
|
||||||
|
|
||||||
# 2. Mise à jour effective du statut vers 'canceled'
|
|
||||||
cur.execute("""
|
|
||||||
UPDATE interventions_intervention i
|
|
||||||
SET status = 'canceled',
|
|
||||||
status_order = 200,
|
|
||||||
cancellation_type = 'non_relevant'
|
|
||||||
FROM interventions_interventionasset ia
|
|
||||||
LEFT JOIN assets_naturetree t ON t.id = ia.object_id AND ia.content_type_id = %s
|
|
||||||
WHERE ia.intervention_id = i.id
|
|
||||||
AND (ia.content_type_id = %s OR i.source_category_id = %s)
|
|
||||||
AND i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND (
|
|
||||||
(t.last_phytosanitary_date IS NOT NULL AND (
|
|
||||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
|
||||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < t.last_phytosanitary_date)
|
|
||||||
))
|
|
||||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < %s)
|
|
||||||
);
|
|
||||||
""", [content_type_tree.id, content_type_tree.id, source_fulcrum_id, cutoff_date])
|
|
||||||
updated_count = cur.rowcount
|
|
||||||
|
|
||||||
# Traitement des interventions orphelines sans lien ia (ex: source Fulcrum sans arbre matché)
|
|
||||||
cur.execute("""
|
|
||||||
UPDATE interventions_intervention i
|
|
||||||
SET status = 'canceled',
|
|
||||||
status_order = 200,
|
|
||||||
cancellation_type = 'non_relevant'
|
|
||||||
WHERE i.source_category_id = %s
|
|
||||||
AND i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND i.expected_end_time IS NOT NULL
|
|
||||||
AND i.expected_end_time::date < %s;
|
|
||||||
""", [source_fulcrum_id, cutoff_date])
|
|
||||||
updated_count += cur.rowcount
|
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(
|
|
||||||
f" {updated_count} interventions passées au statut 'canceled'."
|
|
||||||
))
|
|
||||||
self.stdout.write(f" {tl_diag_count + tl_expired_count} événements d'historique créés dans la timeline.")
|
|
||||||
|
|
||||||
# 3. Recalcul des prochaines dates d'inspection sur les arbres
|
|
||||||
if recalc_trees:
|
|
||||||
self.stdout.write("Recalcul des prochaines inspections sur les arbres...")
|
|
||||||
# 3a. Arbres avec intervention active résiduelle
|
|
||||||
cur.execute("""
|
|
||||||
UPDATE assets_naturetree t
|
|
||||||
SET next_inspection_date = sub.min_date
|
|
||||||
FROM (
|
|
||||||
SELECT ia.object_id AS tree_id, MIN(i.expected_end_time)::date AS min_date
|
|
||||||
FROM interventions_interventionasset ia
|
|
||||||
JOIN interventions_intervention i ON i.id = ia.intervention_id
|
|
||||||
WHERE ia.content_type_id = %s
|
|
||||||
AND i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND i.expected_end_time IS NOT NULL
|
|
||||||
GROUP BY ia.object_id
|
|
||||||
) sub
|
|
||||||
WHERE t.id = sub.tree_id;
|
|
||||||
""", [content_type_tree.id])
|
|
||||||
trees_active = cur.rowcount
|
|
||||||
|
|
||||||
# 3b. Arbres sans intervention active : formule théorique = dernier diagnostic + périodicité
|
|
||||||
cur.execute("""
|
|
||||||
UPDATE assets_naturetree t
|
|
||||||
SET next_inspection_date = (t.last_phytosanitary_date + (COALESCE(t.pruning_frequency_years, 3) || ' years')::interval)::date
|
|
||||||
WHERE t.last_phytosanitary_date IS NOT NULL
|
|
||||||
AND NOT EXISTS (
|
|
||||||
SELECT 1 FROM interventions_interventionasset ia
|
|
||||||
JOIN interventions_intervention i ON i.id = ia.intervention_id
|
|
||||||
WHERE ia.content_type_id = %s
|
|
||||||
AND ia.object_id = t.id
|
|
||||||
AND i.status NOT IN ('finished', 'canceled')
|
|
||||||
AND i.expected_end_time IS NOT NULL
|
|
||||||
);
|
|
||||||
""", [content_type_tree.id])
|
|
||||||
trees_theo = cur.rowcount
|
|
||||||
|
|
||||||
self.stdout.write(f" {trees_active} arbres mis à jour avec prochaine intervention active.")
|
|
||||||
self.stdout.write(f" {trees_theo} arbres recalculés selon cycle théorique (diag + périodicité).")
|
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS("\nOpération terminée avec succès !"))
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
"""
|
|
||||||
Management command: create_structure_folders
|
|
||||||
===========================================
|
|
||||||
Crée les répertoires par défaut de gestion documentaire pour les structures (ouvrages d'art)
|
|
||||||
qui n'en possèdent pas encore (ex: stations et tunnels de métro récemment importés).
|
|
||||||
|
|
||||||
Pour chaque ouvrage d'art traité :
|
|
||||||
1. Crée le dossier racine rattaché à l'ouvrage via DocumentFolderAttachment (is_default=True) :
|
|
||||||
- Nom : "<code_ouvrage> - <nom_ouvrage>" (ex: "MS001 - Station Albert")
|
|
||||||
- Slug : "<code_minuscule>" (ex: "ms001")
|
|
||||||
- Dossier parent : "Structures" (slug: "assets-structures")
|
|
||||||
- Thématique : "structures"
|
|
||||||
- Visibilité : "internal"
|
|
||||||
2. Crée l'arborescence standard de sous-dossiers :
|
|
||||||
- Civil engineering
|
|
||||||
├── Inspection reports
|
|
||||||
└── Exceptional transport
|
|
||||||
- Electromechanics
|
|
||||||
└── Preventive maintenance
|
|
||||||
|
|
||||||
Usage :
|
|
||||||
python manage.py create_structure_folders [--dry-run] [--type TYPE] [--code CODE]
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.db import transaction
|
|
||||||
from django.utils.text import slugify
|
|
||||||
|
|
||||||
from assets.models.structures import Structure
|
|
||||||
from common.models import Thematic
|
|
||||||
from documents.models import DocumentFolder, DocumentFolderAttachment, VisibilityScope
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_structure_folders(structure, creator_user=None, dry_run=False):
|
|
||||||
"""
|
|
||||||
Crée ou s'assure de l'existence du dossier racine et des sous-dossiers
|
|
||||||
standards pour un ouvrage d'art (Structure).
|
|
||||||
Idempotent : ne recrée pas ce qui existe déjà.
|
|
||||||
"""
|
|
||||||
structure_ct = ContentType.objects.get_for_model(Structure)
|
|
||||||
structures_thematic = Thematic.objects.filter(code='structures').first()
|
|
||||||
structures_parent_folder = DocumentFolder.objects.filter(slug='assets-structures').first()
|
|
||||||
|
|
||||||
root_slug = slugify(structure.code)
|
|
||||||
root_folder = DocumentFolder.objects.filter(slug=root_slug).first()
|
|
||||||
|
|
||||||
root_attachment = DocumentFolderAttachment.objects.filter(
|
|
||||||
content_type=structure_ct,
|
|
||||||
object_id=structure.pk,
|
|
||||||
is_default=True
|
|
||||||
).select_related('folder').first()
|
|
||||||
|
|
||||||
if not root_folder and root_attachment:
|
|
||||||
root_folder = root_attachment.folder
|
|
||||||
|
|
||||||
root_name = f"{structure.code} - {structure.get_name()}"
|
|
||||||
root_desc = f"Dossier pour la structure {structure.code} - {structure.get_name()}"
|
|
||||||
|
|
||||||
created_root = False
|
|
||||||
if not root_folder:
|
|
||||||
if not dry_run:
|
|
||||||
root_folder = DocumentFolder.objects.create(
|
|
||||||
name=root_name,
|
|
||||||
slug=root_slug,
|
|
||||||
description=root_desc,
|
|
||||||
visibility=VisibilityScope.INTERNAL,
|
|
||||||
is_predefined=True,
|
|
||||||
application_label='structures',
|
|
||||||
created_by=creator_user
|
|
||||||
)
|
|
||||||
if structures_thematic:
|
|
||||||
root_folder.thematics.add(structures_thematic)
|
|
||||||
if structures_parent_folder:
|
|
||||||
root_folder.parent_folders.add(structures_parent_folder)
|
|
||||||
created_root = True
|
|
||||||
|
|
||||||
if not dry_run and root_folder and not root_attachment:
|
|
||||||
DocumentFolderAttachment.objects.get_or_create(
|
|
||||||
content_type=structure_ct,
|
|
||||||
object_id=structure.pk,
|
|
||||||
folder=root_folder,
|
|
||||||
defaults={'is_default': True}
|
|
||||||
)
|
|
||||||
|
|
||||||
subfolders_created = 0
|
|
||||||
|
|
||||||
def get_or_create_subfolder(parent, name, sub_suffix):
|
|
||||||
nonlocal subfolders_created
|
|
||||||
base_slug = f"{root_slug}-{sub_suffix}"[:150]
|
|
||||||
sub = DocumentFolder.objects.filter(slug=base_slug).first()
|
|
||||||
if not sub and parent and getattr(parent, 'pk', None):
|
|
||||||
sub = parent.child_folders.filter(name=name).first()
|
|
||||||
|
|
||||||
if not sub:
|
|
||||||
if not dry_run:
|
|
||||||
sub = DocumentFolder.objects.create(
|
|
||||||
name=name,
|
|
||||||
slug=base_slug,
|
|
||||||
visibility=VisibilityScope.INTERNAL,
|
|
||||||
is_predefined=False,
|
|
||||||
application_label='structures',
|
|
||||||
created_by=creator_user
|
|
||||||
)
|
|
||||||
if structures_thematic:
|
|
||||||
sub.thematics.add(structures_thematic)
|
|
||||||
if parent and getattr(parent, 'pk', None):
|
|
||||||
sub.parent_folders.add(parent)
|
|
||||||
subfolders_created += 1
|
|
||||||
return sub, True
|
|
||||||
return sub, False
|
|
||||||
|
|
||||||
# 1. Civil engineering & sous-dossiers
|
|
||||||
civil_folder, _ = get_or_create_subfolder(root_folder, "Civil engineering", "civil-engineering")
|
|
||||||
get_or_create_subfolder(civil_folder, "Inspection reports", "civil-engineering-inspection-reports")
|
|
||||||
get_or_create_subfolder(civil_folder, "Exceptional transport", "civil-engineering-exceptional-transport")
|
|
||||||
|
|
||||||
# 2. Electromechanics & sous-dossiers
|
|
||||||
em_folder, _ = get_or_create_subfolder(root_folder, "Electromechanics", "electromechanics")
|
|
||||||
get_or_create_subfolder(em_folder, "Preventive maintenance", "electromechanics-preventive-maintenance")
|
|
||||||
|
|
||||||
return {
|
|
||||||
'root_folder': root_folder,
|
|
||||||
'created_root': created_root,
|
|
||||||
'subfolders_created': subfolders_created
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Crée les répertoires par défaut de gestion documentaire pour les structures."
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument(
|
|
||||||
'--dry-run',
|
|
||||||
action='store_true',
|
|
||||||
help="Simule l'exécution sans effectuer d'écritures en base de données."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--type',
|
|
||||||
dest='structure_type',
|
|
||||||
help="Filtre par type de structure (ex: metro_station,metro_tunnel)."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--code',
|
|
||||||
dest='structure_code',
|
|
||||||
help="Cible un ouvrage spécifique via son code (ex: MS001)."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--user',
|
|
||||||
dest='username',
|
|
||||||
help="Nom d'utilisateur créateur des dossiers (par défaut kdeterme ou superuser)."
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
dry_run = options['dry_run']
|
|
||||||
structure_type = options.get('structure_type')
|
|
||||||
structure_code = options.get('structure_code')
|
|
||||||
username = options.get('username')
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
self.stdout.write(self.style.WARNING("=== Mode simulation (--dry-run) activé. Aucune modification en base. ==="))
|
|
||||||
|
|
||||||
creator_user = None
|
|
||||||
if username:
|
|
||||||
creator_user = User.objects.filter(username=username).first()
|
|
||||||
if not creator_user:
|
|
||||||
creator_user = User.objects.filter(username="kdeterme").first() or User.objects.filter(is_superuser=True).first()
|
|
||||||
|
|
||||||
qs = Structure.objects.all().order_by('code')
|
|
||||||
|
|
||||||
if structure_code:
|
|
||||||
qs = qs.filter(code__iexact=structure_code)
|
|
||||||
elif structure_type:
|
|
||||||
types = [t.strip() for t in structure_type.split(',')]
|
|
||||||
qs = qs.filter(structure_type__in=types)
|
|
||||||
|
|
||||||
structure_ct = ContentType.objects.get_for_model(Structure)
|
|
||||||
existing_attached_ids = set(
|
|
||||||
DocumentFolderAttachment.objects.filter(
|
|
||||||
content_type=structure_ct,
|
|
||||||
is_default=True
|
|
||||||
).values_list('object_id', flat=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
missing_structures = [s for s in qs if s.pk not in existing_attached_ids]
|
|
||||||
|
|
||||||
self.stdout.write(f"Structures analysées : {qs.count()} | Sans répertoire par défaut : {len(missing_structures)}")
|
|
||||||
|
|
||||||
total_roots = 0
|
|
||||||
total_subfolders = 0
|
|
||||||
|
|
||||||
target_structures = missing_structures if not structure_code else list(qs)
|
|
||||||
|
|
||||||
for structure in target_structures:
|
|
||||||
res = ensure_structure_folders(structure, creator_user=creator_user, dry_run=dry_run)
|
|
||||||
if res['created_root']:
|
|
||||||
total_roots += 1
|
|
||||||
total_subfolders += res['subfolders_created']
|
|
||||||
self.stdout.write(f" ✓ [{structure.code}] {structure.get_name()} -> Dossier racine et arborescence configurés")
|
|
||||||
|
|
||||||
status_label = "[Simulation] Répertoires qui seraient créés" if dry_run else "Répertoires créés avec succès"
|
|
||||||
self.stdout.write(self.style.SUCCESS(
|
|
||||||
f"\n=== {status_label} ===\n"
|
|
||||||
f" - Dossiers racines créés : {total_roots}\n"
|
|
||||||
f" - Sous-dossiers créés : {total_subfolders}\n"
|
|
||||||
f" - Total dossiers : {total_roots + total_subfolders}"
|
|
||||||
))
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
from assets.management.commands.import_fulcrum_trees import Command as BaseFulcrumCommand
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseFulcrumCommand):
|
|
||||||
help = "Importe l'inventaire Fulcrum des arbres régionaux (arbres, commandes/gestion, interventions)."
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 6.0.7 on 2026-09-21 11:59
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0026_add_naturetree_specific_fields'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='structure',
|
|
||||||
name='structure_type',
|
|
||||||
field=models.CharField(blank=True, choices=[('tunnel', 'Tunnel'), ('bridge', 'Pont'), ('viaduct', 'Viaduc'), ('overpass', 'Passerelle'), ('underpass', 'Passage souterrain'), ('gallery', 'Galerie technique'), ('atypical', 'Ouvrage atypique'), ('metro_station', 'Station de métro'), ('metro_tunnel', 'Tunnel de métro')], max_length=200, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
# Generated by Django 6.0.7 on 2026-10-05 07:39
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0027_alter_structure_structure_type'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='contractor_name',
|
|
||||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Prestataire / Mainteneur'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='dimensions',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Dimensions / Format'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='energy_source',
|
|
||||||
field=models.CharField(blank=True, choices=[('natural_gas', 'Gaz naturel'), ('electricity', 'Électricité'), ('fuel_oil', 'Fioul domestique'), ('biomass_wood', 'Biomasse / Bois / Pellets'), ('heat_network', 'Réseau de chaleur urbain'), ('heat_pump_air', 'Aérothermie (PAC air-eau / air-air)'), ('heat_pump_geo', 'Géothermie (PAC eau-eau / sol-eau)'), ('solar_thermal', 'Solaire thermique'), ('other', 'Autre source')], max_length=50, null=True, verbose_name="Source d'énergie"),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='fire_rating',
|
|
||||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Classement au feu / Coupe-feu'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='glazing_type',
|
|
||||||
field=models.CharField(blank=True, choices=[('single', 'Simple vitrage'), ('double_standard', 'Double vitrage standard'), ('double_low_e', 'Double vitrage ITR / Haute isolation'), ('triple', 'Triple vitrage'), ('laminated_safety', 'Vitrage feuilleté de sécurité'), ('other', 'Autre vitrage')], max_length=50, null=True, verbose_name='Type de vitrage'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='maintenance_contract_ref',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Réf. contrat maintenance'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='material',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Matériau principal'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='opening_type',
|
|
||||||
field=models.CharField(blank=True, choices=[('hinged', 'Battant / À la française'), ('sliding', 'Coulissant'), ('tilt_and_turn', 'Oscillo-battant'), ('double_action', 'Va-et-vient'), ('sectional_overhead', 'Sectionnel / Basculant'), ('motorized_automatic', 'Automatique / Motorisé'), ('fixed', 'Châssis fixe / Non ouvrant'), ('other', 'Autre type')], max_length=50, null=True, verbose_name="Type d'ouverture"),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='power_kw',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Puissance nominale (kW)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='refrigerant_charge_kg',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Charge fluide frigorigène (kg)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='refrigerant_type',
|
|
||||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Fluide frigorigène'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='regulatory_periodicity_months',
|
|
||||||
field=models.IntegerField(blank=True, null=True, verbose_name='Périodicité contrôle réglementaire (mois)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='replacement_cost_estimate',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Coût estimé de renouvellement (€)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='surface_m2',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Surface unitaire (m²)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='publicbuilding',
|
|
||||||
name='ceiling_height_m',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Hauteur sous plafond (m)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='publicbuilding',
|
|
||||||
name='fire_compartment',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Secteur / Compartiment coupe-feu'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='publicbuilding',
|
|
||||||
name='floor_type',
|
|
||||||
field=models.CharField(blank=True, choices=[('tiles', 'Carrelage / Faïence'), ('parquet', 'Parquet / Bois'), ('linoleum_pvc', 'Linoléum / Vinyle / PVC'), ('concrete', 'Béton lissé / Chape / Résine'), ('carpet', 'Moquette / Textile'), ('other', 'Autre revêtement')], max_length=50, null=True, verbose_name='Revêtement de sol dominant'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='publicbuilding',
|
|
||||||
name='is_heated',
|
|
||||||
field=models.BooleanField(default=True, verbose_name='Local chauffé'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='publicbuilding',
|
|
||||||
name='room_number',
|
|
||||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Numéro de local / porte'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='buildingasset',
|
|
||||||
name='asset_type',
|
|
||||||
field=models.CharField(choices=[('roof', 'Toiture / Couverture / Étanchéité'), ('roof_drainage', 'Zinguerie & Évacuation EP (Gouttière, chéneau)'), ('facade', 'Façade / Bardage / Mur extérieur'), ('window', 'Fenêtre / Baie vitrée / Châssis'), ('skylight', 'Lanterneau / Verrière / Puits de lumière'), ('external_door', "Porte extérieure / Porte d'entrée / Portail"), ('shutter_blind', 'Volet / Store / Brise-soleil (BSO)'), ('insulation', 'Isolation thermique (ITE / ITI)'), ('wall', 'Mur intérieur / Cloison'), ('interior_door', 'Porte intérieure / Bloc-porte'), ('fire_door', 'Porte coupe-feu / Bloc pare-flamme'), ('ceiling', 'Faux-plafond / Plafond suspendu'), ('flooring', 'Revêtement de sol'), ('stairs', 'Escalier / Garde-corps / Main courante'), ('boiler', 'Chaudière (Gaz / Fioul / Biomasse / Pellets)'), ('heat_pump', 'Pompe à chaleur (PAC) / Climatisation réversible'), ('heat_substation', "Sous-station d'échangeur thermique / Réseau urbain"), ('radiator_emitter', 'Émetteur de chaleur (Radiateur, convecteur, plancher)'), ('air_handling_unit', "Centrale de traitement d'air (CTA) / VMC"), ('air_conditioning', 'Climatisation / Groupe froid / Chiller / VRV'), ('water_heater', 'Production Eau Chaude Sanitaire (Ballon ECS)'), ('hvac', 'Chauffage / CVC / Régulation générale'), ('electricity', 'Électricité / TGBT / Tableau divisionnaire'), ('inverter_generator', 'Onduleur / Groupe électrogène de secours'), ('lighting', 'Éclairage intérieur / Luminaires'), ('emergency_lighting', 'Éclairage de sécurité / BAES / BAEH'), ('meter', 'Compteur (Eau / Gaz / Électricité / Calories)'), ('solar_panel', 'Panneaux solaires (Photovoltaïque / Thermique)'), ('ev_charging', 'Borne de recharge véhicule électrique'), ('sanitary', 'Plomberie / Sanitaires (WC, lavabo, douche)'), ('plumbing_piping', "Réseau d'eau / Vanne d'arrêt / Disconnecteur"), ('sump_pump', 'Pompe de relevage (Eaux usées / Eaux pluviales)'), ('rainwater_harvesting', "Récupération d'eau de pluie / Cuve de stockage"), ('fire_safety', 'Sécurité incendie / Centrale SSI / CMSI'), ('smoke_detector', 'Détecteur de fumée / Déclencheur manuel (DM)'), ('fire_extinguisher', "Extincteur / Robinet d'Incendie Armé (RIA)"), ('smoke_exhaust', 'Désenfumage / Trappe / Skydome'), ('access_control', "Contrôle d'accès (Badge, interphone, gâche)"), ('alarm', 'Alarme anti-intrusion / Détecteur de présence'), ('cctv_surveillance', 'Vidéosurveillance / Caméra CCTV'), ('elevator', 'Ascenseur / Monte-charge'), ('disabled_lift', 'Élévateur PMR / Plateforme élévatrice'), ('network_rack', 'Baie de brassage / Rack informatique / RJ45'), ('bms_system', 'Gestion Technique du Bâtiment (GTB / GTC)'), ('civil_engineering', 'Génie civil / Bâti / Gros œuvre'), ('other', 'Autre équipement / asset')], default='civil_engineering', max_length=50, verbose_name="Type d'asset / Équipement"),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='publicbuilding',
|
|
||||||
name='location_type',
|
|
||||||
field=models.CharField(choices=[('site', 'Site / Complexe'), ('building', 'Bâtiment / Bloc'), ('wing', 'Aile / Corps de bâtiment'), ('level', 'Niveau / Étage'), ('basement', 'Sous-sol / Cave'), ('attic', 'Combles / Grenier / Toiture-terrasse'), ('mezzanine', 'Mezzanine'), ('stairwell', "Cage d'escalier"), ('corridor', 'Couloir / Coursive / Dégagement'), ('lobby', "Hall d'accueil / Entrée / Sas"), ('emergency_exit', "Issue de secours / Sas d'évacuation"), ('elevator_shaft', "Gaine / Palier d'ascenseur"), ('technical_room', 'Local technique général'), ('boiler_room', 'Chaufferie / Sous-station thermique'), ('electrical_room', 'Local TGBT / Électrique'), ('ventilation_room', 'Local ventilation / CTA'), ('elevator_machine_room', 'Local machinerie ascenseur'), ('server_room', 'Salle serveurs / Baie informatique'), ('room', 'Pièce / Local polyvalent'), ('office', 'Bureau / Espace de travail'), ('meeting_room', 'Salle de réunion / Conférence'), ('classroom', 'Salle de classe / Formation'), ('workshop', 'Atelier / Laboratoire'), ('sanitary_room', 'Sanitaires / Vestiaires / Douches'), ('storage_room', 'Réserve / Stockage / Archives'), ('canteen_kitchen', 'Cuisine / Réfectoire / Restauration'), ('cleaning_room', 'Local ménage / Entretien'), ('waste_room', 'Local poubelles / Déchets'), ('indoor_parking', 'Parking couvert / Garage'), ('outdoor_area', 'Abords / Cour / Parvis / Terrasse'), ('other', 'Autre localisation')], default='building', max_length=50, verbose_name='Type de localisation'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# Generated by Django 6.0.7 on 2026-10-05 08:25
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0028_buildingasset_contractor_name_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='publicbuildingdocument',
|
|
||||||
name='floor_location',
|
|
||||||
field=models.ForeignKey(blank=True, help_text='Niveau ou local auquel ce document/plan se rapporte.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='floor_documents', to='assets.publicbuilding', verbose_name='Niveau / Étage concerné'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='publicbuildingdocument',
|
|
||||||
name='document_type',
|
|
||||||
field=models.CharField(choices=[('floor_plan', "Plan d'étage / Plan architectural"), ('as_built', 'Plan de récolement (As-Built)'), ('erp_notice', 'Notice de sécurité / Dossier ERP'), ('dpe', 'Diagnostic énergétique (DPE / PEB)'), ('technical_notice', "Notice technique / Manuel d'exploitation"), ('other', 'Autre document')], default='other', max_length=50, verbose_name='Type de document'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
# Generated by Django 6.0.7 on 2026-10-05 13:06
|
|
||||||
|
|
||||||
import common.storages
|
|
||||||
import django.contrib.gis.db.models.fields
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0029_publicbuildingdocument_floor_location_and_more'),
|
|
||||||
('common', '0017_territory'),
|
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
('documents', '0004_alter_documentfolder_visibility_and_more'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ManagementZone',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('code', models.CharField(max_length=100, unique=True, verbose_name='Code')),
|
|
||||||
('name_fr', models.CharField(max_length=255, verbose_name='Nom (FR)')),
|
|
||||||
('name_nl', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom (NL)')),
|
|
||||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
|
||||||
('instructions', models.TextField(blank=True, help_text="Consignes d'exploitation, points de vigilance, règles de sécurité...", null=True, verbose_name="Consignes et points d'attention")),
|
|
||||||
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Périmètre géographique')),
|
|
||||||
('color', models.CharField(blank=True, help_text='Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone.', max_length=20, null=True, verbose_name="Couleur d'affichage")),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
|
||||||
('target_frequency_days', models.IntegerField(blank=True, help_text='Intervalle indicatif entre interventions en jours.', null=True, verbose_name='Fréquence cible (jours)')),
|
|
||||||
('seasonality_notes', models.CharField(blank=True, help_text="Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps.", max_length=255, null=True, verbose_name='Saisonnalité / Calendrier')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Date de création')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Date de mise à jour')),
|
|
||||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_management_zones', to=settings.AUTH_USER_MODEL, verbose_name='Créé par')),
|
|
||||||
('thematic', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='management_zones', to='common.thematic', verbose_name='Thématique')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Zone de gestion',
|
|
||||||
'verbose_name_plural': 'Zones de gestion',
|
|
||||||
'ordering': ['thematic', 'code'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ManagementZoneDocument',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('description', models.TextField(blank=True, null=True, verbose_name='Description du document')),
|
|
||||||
('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')),
|
|
||||||
('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')),
|
|
||||||
('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/management_zones/', verbose_name='Fichier')),
|
|
||||||
('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/management_zones/')),
|
|
||||||
('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')),
|
|
||||||
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')),
|
|
||||||
('zone', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.managementzone', verbose_name='Zone de gestion')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Document de zone de gestion',
|
|
||||||
'verbose_name_plural': 'Documents de zones de gestion',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ManagementZoneType',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('code', models.CharField(max_length=100, unique=True, verbose_name='Code')),
|
|
||||||
('name_fr', models.CharField(max_length=255, verbose_name='Nom (FR)')),
|
|
||||||
('name_nl', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom (NL)')),
|
|
||||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
|
||||||
('default_color', models.CharField(default='#198754', max_length=20, verbose_name='Couleur par défaut')),
|
|
||||||
('thematic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='management_zone_types', to='common.thematic', verbose_name='Thématique')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Type de zone de gestion',
|
|
||||||
'verbose_name_plural': 'Types de zones de gestion',
|
|
||||||
'ordering': ['thematic', 'name_fr'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='managementzone',
|
|
||||||
name='zone_type',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='management_zones', to='assets.managementzonetype', verbose_name='Type de zone de gestion'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ManagementZoneAsset',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('object_id', models.PositiveIntegerField(verbose_name="ID de l'asset")),
|
|
||||||
('is_excluded', models.BooleanField(default=False, help_text='Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre.', verbose_name='Exclu')),
|
|
||||||
('notes', models.CharField(blank=True, max_length=255, null=True, verbose_name='Notes')),
|
|
||||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset")),
|
|
||||||
('management_zone', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='explicit_assets', to='assets.managementzone', verbose_name='Zone de gestion')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Asset de zone de gestion',
|
|
||||||
'verbose_name_plural': 'Assets de zones de gestion',
|
|
||||||
'unique_together': {('management_zone', 'content_type', 'object_id')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
# Generated by Django 6.0.7 on 2026-10-08 10:19
|
|
||||||
|
|
||||||
import assets.models.core
|
|
||||||
import django.contrib.gis.db.models.fields
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0030_managementzone_managementzonedocument_and_more'),
|
|
||||||
('contracts', '0014_contract_color_contract_geom'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='itsgeoasset',
|
|
||||||
name='extra_data',
|
|
||||||
field=models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ITSBollardSystem',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('code', models.CharField(max_length=50)),
|
|
||||||
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
|
||||||
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
|
||||||
('external_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe')),
|
|
||||||
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
|
|
||||||
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
|
|
||||||
('installation_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
|
|
||||||
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
|
|
||||||
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('inspection_status', models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection")),
|
|
||||||
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('last_survey_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('brand', models.CharField(blank=True, max_length=500, null=True)),
|
|
||||||
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
|
|
||||||
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
|
|
||||||
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
|
|
||||||
('funding_program', models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('lon', models.FloatField(blank=True, null=True)),
|
|
||||||
('lat', models.FloatField(blank=True, null=True)),
|
|
||||||
('geojson', models.TextField(blank=True, null=True)),
|
|
||||||
('geom', django.contrib.gis.db.models.fields.LineStringField(blank=True, null=True, srid=3812, verbose_name='Géométrie (Ligne)')),
|
|
||||||
('width', models.FloatField(blank=True, null=True, verbose_name='Largeur de passage (m)')),
|
|
||||||
('direction', models.CharField(blank=True, choices=[('unidirectional', 'Sens unique'), ('bidirectional', 'Double sens')], default='bidirectional', max_length=50, null=True, verbose_name='Sens de circulation')),
|
|
||||||
('extra_data', models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques')),
|
|
||||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
|
||||||
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bollard_systems', to='assets.itslocation', verbose_name='Localisation ITS')),
|
|
||||||
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
|
|
||||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel', verbose_name='Modèle de système')),
|
|
||||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsbollardsystem', verbose_name='Remplacé par')),
|
|
||||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Système de bornes',
|
|
||||||
'verbose_name_plural': 'Systèmes de bornes',
|
|
||||||
},
|
|
||||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ITSBollardSystemAsset',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('role', models.CharField(choices=[('camera_entry', 'Caméra entrée (LAPI)'), ('camera_exit', 'Caméra sortie (LAPI)'), ('camera_cctv', 'Caméra de contexte / surveillance'), ('cabinet', 'Armoire électrique / automate'), ('totem', 'Totem de commande'), ('traffic_light', 'Feu de franchissement'), ('loop', 'Boucle de détection'), ('other', 'Autre')], default='other', max_length=50, verbose_name="Rôle de l'équipement")),
|
|
||||||
('comment', models.CharField(blank=True, max_length=255, null=True, verbose_name='Commentaire')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name="Date d'association")),
|
|
||||||
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_system_links', to='assets.itsgeoasset', verbose_name='Équipement ITS')),
|
|
||||||
('bollard_system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='system_assets', to='assets.itsbollardsystem', verbose_name='Système de bornes')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Équipement associé au système de bornes',
|
|
||||||
'verbose_name_plural': 'Équipements associés au système de bornes',
|
|
||||||
'unique_together': {('bollard_system', 'asset')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='itsbollardsystem',
|
|
||||||
name='associated_assets',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='bollard_systems', through='assets.ITSBollardSystemAsset', to='assets.itsgeoasset', verbose_name='Équipements associés'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ITSBollardSystemContract',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)),
|
|
||||||
('maintenance_type', models.CharField(choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other', max_length=50)),
|
|
||||||
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_systems_contracts', to='contracts.contract')),
|
|
||||||
('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_systems_contracts', to='assets.itsbollardsystem')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Contrat de système de bornes',
|
|
||||||
'verbose_name_plural': 'Contrats de systèmes de bornes',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ITSBollardPlot',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('code', models.CharField(max_length=50)),
|
|
||||||
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
|
||||||
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
|
||||||
('external_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe')),
|
|
||||||
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
|
|
||||||
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
|
|
||||||
('installation_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
|
|
||||||
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
|
|
||||||
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('inspection_status', models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection")),
|
|
||||||
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('last_survey_date', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('brand', models.CharField(blank=True, max_length=500, null=True)),
|
|
||||||
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
|
|
||||||
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
|
|
||||||
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
|
|
||||||
('funding_program', models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('lon', models.FloatField(blank=True, null=True)),
|
|
||||||
('lat', models.FloatField(blank=True, null=True)),
|
|
||||||
('geojson', models.TextField(blank=True, null=True)),
|
|
||||||
('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812, verbose_name='Géométrie (Point)')),
|
|
||||||
('position', models.PositiveSmallIntegerField(default=1, help_text='Ordre de gauche à droite (1, 2, 3...)', verbose_name='Position sur la traverse')),
|
|
||||||
('bollard_type', models.CharField(choices=[('automatic', 'Automatique / Escamotable'), ('semi_automatic', 'Semi-automatique'), ('fixed', 'Fixe'), ('removable', 'Amovible')], default='automatic', max_length=50, verbose_name='Type de plot')),
|
|
||||||
('extra_data', models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques')),
|
|
||||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
|
||||||
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bollard_plots', to='assets.itslocation', verbose_name='Localisation ITS')),
|
|
||||||
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
|
|
||||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel', verbose_name='Modèle de plot')),
|
|
||||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsbollardplot', verbose_name='Remplacé par')),
|
|
||||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
|
||||||
('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plots', to='assets.itsbollardsystem', verbose_name='Système de bornes')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Plot de borne',
|
|
||||||
'verbose_name_plural': 'Plots de bornes',
|
|
||||||
'ordering': ['system', 'position'],
|
|
||||||
'indexes': [models.Index(fields=['code'], name='assets_itsb_code_de6000_idx')],
|
|
||||||
},
|
|
||||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='itsbollardsystem',
|
|
||||||
index=models.Index(fields=['code'], name='assets_itsb_code_a4c148_idx'),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='itsbollardsystemcontract',
|
|
||||||
unique_together={('system', 'contract')},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
# Generated by Django 6.0.7 on 2026-10-07 15:23
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0030_managementzone_managementzonedocument_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='amenity_value',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name="Valeur d'aménité (€)"),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='biodiversity_index',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Indice de biodiversité'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='carbon_stock',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Stock de carbone (t CO2)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='cooling_energy_indicator',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Énergie de refroidissement (kWh)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='cooling_indicator',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Indice de rafraîchissement'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='cultivar',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Cultivar / Variété'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='death_date',
|
|
||||||
field=models.DateField(blank=True, null=True, verbose_name='Date de dépérissement / mort'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='genus',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Genre botanique'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='is_chronoxyle',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='Arbre sénescent / Chronoxyle (bois mort sur pied)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='is_dead',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='Arbre mort'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='is_stump',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='Souche'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='management_type',
|
|
||||||
field=models.CharField(blank=True, choices=[('municipal', 'Communal (Régie communale)'), ('regional', 'Régional (Bruxelles Mobilité / Région)'), ('community', 'Intercommunal / Autre pouvoir public'), ('private', 'Privé / Tiers')], default='municipal', max_length=50, null=True, verbose_name='Type de gestionnaire'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='managing_authority',
|
|
||||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Autorité gestionnaire'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='number_of_trunks',
|
|
||||||
field=models.IntegerField(blank=True, default=1, null=True, verbose_name='Nombre de troncs / Cépée'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='plantation_coefficient',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Coefficient de plantation'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='planting_date',
|
|
||||||
field=models.DateField(blank=True, null=True, verbose_name='Date de plantation réelle'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='scientific_name',
|
|
||||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Nom scientifique / latin'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='situation_coefficient',
|
|
||||||
field=models.FloatField(blank=True, null=True, verbose_name='Coefficient de situation'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='naturetree',
|
|
||||||
name='vernacular_name',
|
|
||||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Nom vernaculaire / Essence'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# Generated by Django 6.0.7 on 2026-10-09 11:48
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0031_itsgeoasset_extra_data_itsbollardsystem_and_more'),
|
|
||||||
('assets', '0031_naturetree_amenity_value_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
]
|
|
||||||
|
|
@ -8,10 +8,9 @@ from .structures import Structure, StructureLocation, StructureGeoAsset, Structu
|
||||||
from .controlcenters import ControlCenter, ControlCenterLocation, ControlCenterGeoAsset, ControlCenterAssetModel, ControlCenterDocument, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument
|
from .controlcenters import ControlCenter, ControlCenterLocation, ControlCenterGeoAsset, ControlCenterAssetModel, ControlCenterDocument, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument
|
||||||
from .sign import SignStreet, SignStreetDocument, SignPole, SignPoleModel, SignPanel, SignPanelModel, SignPanelType, ComplementaryPoliceRegulation, SignParkingSpot
|
from .sign import SignStreet, SignStreetDocument, SignPole, SignPoleModel, SignPanel, SignPanelModel, SignPanelType, ComplementaryPoliceRegulation, SignParkingSpot
|
||||||
from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain
|
from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain
|
||||||
from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract, ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract
|
||||||
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
|
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
|
||||||
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
|
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
|
||||||
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
||||||
from .parking import ParkingLocation, ParkingSpot
|
from .parking import ParkingLocation, ParkingSpot
|
||||||
from inspections.models import Inspection as AssetInspection, ParkingSpotInspection
|
from inspections.models import Inspection as AssetInspection, ParkingSpotInspection
|
||||||
from .management_zones import ManagementZone, ManagementZoneType, ManagementZoneAsset, ManagementZoneDocument
|
|
||||||
|
|
|
||||||
|
|
@ -77,148 +77,31 @@ ERP_TYPE_CHOICES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
LOCATION_TYPE_CHOICES = (
|
LOCATION_TYPE_CHOICES = (
|
||||||
# Ensembles & Bâtiments
|
|
||||||
('site', _('Site / Complexe')),
|
('site', _('Site / Complexe')),
|
||||||
('building', _('Bâtiment / Bloc')),
|
('building', _('Bâtiment / Bloc')),
|
||||||
('wing', _('Aile / Corps de bâtiment')),
|
|
||||||
# Niveaux & Hauteurs
|
|
||||||
('level', _('Niveau / Étage')),
|
('level', _('Niveau / Étage')),
|
||||||
('basement', _('Sous-sol / Cave')),
|
('room', _('Pièce / Local')),
|
||||||
('attic', _('Combles / Grenier / Toiture-terrasse')),
|
|
||||||
('mezzanine', _('Mezzanine')),
|
|
||||||
# Circulations & Dégagements
|
|
||||||
('stairwell', _('Cage d\'escalier')),
|
|
||||||
('corridor', _('Couloir / Coursive / Dégagement')),
|
|
||||||
('lobby', _('Hall d\'accueil / Entrée / Sas')),
|
|
||||||
('emergency_exit', _('Issue de secours / Sas d\'évacuation')),
|
|
||||||
('elevator_shaft', _('Gaine / Palier d\'ascenseur')),
|
|
||||||
# Locaux techniques
|
|
||||||
('technical_room', _('Local technique général')),
|
|
||||||
('boiler_room', _('Chaufferie / Sous-station thermique')),
|
|
||||||
('electrical_room', _('Local TGBT / Électrique')),
|
|
||||||
('ventilation_room', _('Local ventilation / CTA')),
|
|
||||||
('elevator_machine_room', _('Local machinerie ascenseur')),
|
|
||||||
('server_room', _('Salle serveurs / Baie informatique')),
|
|
||||||
# Locaux de travail, d'accueil & services
|
|
||||||
('room', _('Pièce / Local polyvalent')),
|
|
||||||
('office', _('Bureau / Espace de travail')),
|
|
||||||
('meeting_room', _('Salle de réunion / Conférence')),
|
|
||||||
('classroom', _('Salle de classe / Formation')),
|
|
||||||
('workshop', _('Atelier / Laboratoire')),
|
|
||||||
('sanitary_room', _('Sanitaires / Vestiaires / Douches')),
|
|
||||||
('storage_room', _('Réserve / Stockage / Archives')),
|
|
||||||
('canteen_kitchen', _('Cuisine / Réfectoire / Restauration')),
|
|
||||||
('cleaning_room', _('Local ménage / Entretien')),
|
|
||||||
('waste_room', _('Local poubelles / Déchets')),
|
|
||||||
('indoor_parking', _('Parking couvert / Garage')),
|
|
||||||
# Espaces extérieurs immédiats
|
|
||||||
('outdoor_area', _('Abords / Cour / Parvis / Terrasse')),
|
|
||||||
# Autre
|
|
||||||
('other', _('Autre localisation')),
|
('other', _('Autre localisation')),
|
||||||
)
|
)
|
||||||
|
|
||||||
FLOOR_TYPE_CHOICES = (
|
|
||||||
('tiles', _('Carrelage / Faïence')),
|
|
||||||
('parquet', _('Parquet / Bois')),
|
|
||||||
('linoleum_pvc', _('Linoléum / Vinyle / PVC')),
|
|
||||||
('concrete', _('Béton lissé / Chape / Résine')),
|
|
||||||
('carpet', _('Moquette / Textile')),
|
|
||||||
('other', _('Autre revêtement')),
|
|
||||||
)
|
|
||||||
|
|
||||||
BUILDING_ASSET_TYPE_CHOICES = (
|
BUILDING_ASSET_TYPE_CHOICES = (
|
||||||
# Clos, couvert & enveloppe
|
('civil_engineering', _('Génie civil / Bâti')),
|
||||||
('roof', _('Toiture / Couverture / Étanchéité')),
|
('facade', _('Façade / Mur extérieur')),
|
||||||
('roof_drainage', _('Zinguerie & Évacuation EP (Gouttière, chéneau)')),
|
|
||||||
('facade', _('Façade / Bardage / Mur extérieur')),
|
|
||||||
('window', _('Fenêtre / Baie vitrée / Châssis')),
|
|
||||||
('skylight', _('Lanterneau / Verrière / Puits de lumière')),
|
|
||||||
('external_door', _('Porte extérieure / Porte d\'entrée / Portail')),
|
|
||||||
('shutter_blind', _('Volet / Store / Brise-soleil (BSO)')),
|
|
||||||
('insulation', _('Isolation thermique (ITE / ITI)')),
|
|
||||||
# Second œuvre & aménagements intérieurs
|
|
||||||
('wall', _('Mur intérieur / Cloison')),
|
('wall', _('Mur intérieur / Cloison')),
|
||||||
('interior_door', _('Porte intérieure / Bloc-porte')),
|
('roof', _('Toiture / Couverture')),
|
||||||
('fire_door', _('Porte coupe-feu / Bloc pare-flamme')),
|
('hvac', _('Chauffage / CVC / Chaudière')),
|
||||||
('ceiling', _('Faux-plafond / Plafond suspendu')),
|
|
||||||
('flooring', _('Revêtement de sol')),
|
|
||||||
('stairs', _('Escalier / Garde-corps / Main courante')),
|
|
||||||
# Génie climatique & thermique (CVC)
|
|
||||||
('boiler', _('Chaudière (Gaz / Fioul / Biomasse / Pellets)')),
|
|
||||||
('heat_pump', _('Pompe à chaleur (PAC) / Climatisation réversible')),
|
|
||||||
('heat_substation', _('Sous-station d\'échangeur thermique / Réseau urbain')),
|
|
||||||
('radiator_emitter', _('Émetteur de chaleur (Radiateur, convecteur, plancher)')),
|
|
||||||
('air_handling_unit', _('Centrale de traitement d\'air (CTA) / VMC')),
|
|
||||||
('air_conditioning', _('Climatisation / Groupe froid / Chiller / VRV')),
|
|
||||||
('water_heater', _('Production Eau Chaude Sanitaire (Ballon ECS)')),
|
|
||||||
('hvac', _('Chauffage / CVC / Régulation générale')),
|
|
||||||
# Électricité, Énergie & Réseaux
|
|
||||||
('electricity', _('Électricité / TGBT / Tableau divisionnaire')),
|
|
||||||
('inverter_generator', _('Onduleur / Groupe électrogène de secours')),
|
|
||||||
('lighting', _('Éclairage intérieur / Luminaires')),
|
|
||||||
('emergency_lighting', _('Éclairage de sécurité / BAES / BAEH')),
|
|
||||||
('meter', _('Compteur (Eau / Gaz / Électricité / Calories)')),
|
|
||||||
('solar_panel', _('Panneaux solaires (Photovoltaïque / Thermique)')),
|
|
||||||
('ev_charging', _('Borne de recharge véhicule électrique')),
|
|
||||||
# Plomberie & Sanitaires
|
|
||||||
('sanitary', _('Plomberie / Sanitaires (WC, lavabo, douche)')),
|
|
||||||
('plumbing_piping', _('Réseau d\'eau / Vanne d\'arrêt / Disconnecteur')),
|
|
||||||
('sump_pump', _('Pompe de relevage (Eaux usées / Eaux pluviales)')),
|
|
||||||
('rainwater_harvesting', _('Récupération d\'eau de pluie / Cuve de stockage')),
|
|
||||||
# Sécurité incendie, sûreté & contrôle d'accès
|
|
||||||
('fire_safety', _('Sécurité incendie / Centrale SSI / CMSI')),
|
|
||||||
('smoke_detector', _('Détecteur de fumée / Déclencheur manuel (DM)')),
|
|
||||||
('fire_extinguisher', _('Extincteur / Robinet d\'Incendie Armé (RIA)')),
|
|
||||||
('smoke_exhaust', _('Désenfumage / Trappe / Skydome')),
|
|
||||||
('access_control', _('Contrôle d\'accès (Badge, interphone, gâche)')),
|
|
||||||
('alarm', _('Alarme anti-intrusion / Détecteur de présence')),
|
|
||||||
('cctv_surveillance', _('Vidéosurveillance / Caméra CCTV')),
|
|
||||||
# Mobilité verticale
|
|
||||||
('elevator', _('Ascenseur / Monte-charge')),
|
('elevator', _('Ascenseur / Monte-charge')),
|
||||||
('disabled_lift', _('Élévateur PMR / Plateforme élévatrice')),
|
('electricity', _('Électricité / TGBT')),
|
||||||
# Réseaux, GTB & Automatismes
|
('fire_safety', _('Sécurité incendie / Extincteur / RIA')),
|
||||||
('network_rack', _('Baie de brassage / Rack informatique / RJ45')),
|
('alarm', _('Alarme / Contrôle d\'accès')),
|
||||||
('bms_system', _('Gestion Technique du Bâtiment (GTB / GTC)')),
|
('meter', _('Compteur (Eau / Gaz / Électricité)')),
|
||||||
# Gros œuvre & Divers
|
('sanitary', _('Plomberie / Sanitaires')),
|
||||||
('civil_engineering', _('Génie civil / Bâti / Gros œuvre')),
|
|
||||||
('other', _('Autre équipement / asset')),
|
('other', _('Autre équipement / asset')),
|
||||||
)
|
)
|
||||||
|
|
||||||
# For backward compatibility
|
# For backward compatibility
|
||||||
EQUIPMENT_TYPE_CHOICES = BUILDING_ASSET_TYPE_CHOICES
|
EQUIPMENT_TYPE_CHOICES = BUILDING_ASSET_TYPE_CHOICES
|
||||||
|
|
||||||
ENERGY_SOURCE_CHOICES = (
|
|
||||||
('natural_gas', _('Gaz naturel')),
|
|
||||||
('electricity', _('Électricité')),
|
|
||||||
('fuel_oil', _('Fioul domestique')),
|
|
||||||
('biomass_wood', _('Biomasse / Bois / Pellets')),
|
|
||||||
('heat_network', _('Réseau de chaleur urbain')),
|
|
||||||
('heat_pump_air', _('Aérothermie (PAC air-eau / air-air)')),
|
|
||||||
('heat_pump_geo', _('Géothermie (PAC eau-eau / sol-eau)')),
|
|
||||||
('solar_thermal', _('Solaire thermique')),
|
|
||||||
('other', _('Autre source')),
|
|
||||||
)
|
|
||||||
|
|
||||||
GLAZING_TYPE_CHOICES = (
|
|
||||||
('single', _('Simple vitrage')),
|
|
||||||
('double_standard', _('Double vitrage standard')),
|
|
||||||
('double_low_e', _('Double vitrage ITR / Haute isolation')),
|
|
||||||
('triple', _('Triple vitrage')),
|
|
||||||
('laminated_safety', _('Vitrage feuilleté de sécurité')),
|
|
||||||
('other', _('Autre vitrage')),
|
|
||||||
)
|
|
||||||
|
|
||||||
OPENING_TYPE_CHOICES = (
|
|
||||||
('hinged', _('Battant / À la française')),
|
|
||||||
('sliding', _('Coulissant')),
|
|
||||||
('tilt_and_turn', _('Oscillo-battant')),
|
|
||||||
('double_action', _('Va-et-vient')),
|
|
||||||
('sectional_overhead', _('Sectionnel / Basculant')),
|
|
||||||
('motorized_automatic', _('Automatique / Motorisé')),
|
|
||||||
('fixed', _('Châssis fixe / Non ouvrant')),
|
|
||||||
('other', _('Autre type')),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PublicBuilding(AbstractLocation):
|
class PublicBuilding(AbstractLocation):
|
||||||
"""
|
"""
|
||||||
|
|
@ -250,11 +133,6 @@ class PublicBuilding(AbstractLocation):
|
||||||
usable_surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface utile (m²)"))
|
usable_surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface utile (m²)"))
|
||||||
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface spécifique (m²)"))
|
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface spécifique (m²)"))
|
||||||
level_number = models.IntegerField(default=0, verbose_name=_("Numéro d'étage (ex: 0=RDC, 1=1er, -1=Sous-sol)"))
|
level_number = models.IntegerField(default=0, verbose_name=_("Numéro d'étage (ex: 0=RDC, 1=1er, -1=Sous-sol)"))
|
||||||
room_number = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Numéro de local / porte"))
|
|
||||||
ceiling_height_m = models.FloatField(null=True, blank=True, verbose_name=_("Hauteur sous plafond (m)"))
|
|
||||||
is_heated = models.BooleanField(default=True, verbose_name=_("Local chauffé"))
|
|
||||||
fire_compartment = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Secteur / Compartiment coupe-feu"))
|
|
||||||
floor_type = models.CharField(max_length=50, choices=FLOOR_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Revêtement de sol dominant"))
|
|
||||||
floor_plan = models.FileField(upload_to='buildings/plans/%Y/%m/', storage=private_storage, null=True, blank=True, verbose_name=_("Plan d'étage"))
|
floor_plan = models.FileField(upload_to='buildings/plans/%Y/%m/', storage=private_storage, null=True, blank=True, verbose_name=_("Plan d'étage"))
|
||||||
|
|
||||||
# Réglementation & Accueil du Public
|
# Réglementation & Accueil du Public
|
||||||
|
|
@ -275,26 +153,9 @@ class PublicBuilding(AbstractLocation):
|
||||||
verbose_name_plural = _("Localisations Bâtiments publics")
|
verbose_name_plural = _("Localisations Bâtiments publics")
|
||||||
ordering = ['code', 'name_fr']
|
ordering = ['code', 'name_fr']
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.name_fr or self.name_nl or self.code
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('assets:publicbuilding_detail', kwargs={'pk': self.pk})
|
return reverse('assets:publicbuilding_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def get_floor_plan_url(self):
|
|
||||||
"""
|
|
||||||
Renvoie l'URL du plan d'étage s'il existe (via floor_plan ou document associé de type floor_plan).
|
|
||||||
"""
|
|
||||||
if self.floor_plan and self.floor_plan.name:
|
|
||||||
return reverse('assets:publicbuilding_plan_file', kwargs={'pk': self.pk})
|
|
||||||
docs = getattr(self, 'floor_documents', None)
|
|
||||||
if docs:
|
|
||||||
first_doc = docs.filter(document_type='floor_plan').first()
|
|
||||||
if first_doc and first_doc.file and first_doc.file.name:
|
|
||||||
return reverse('assets:publicbuilding_plan_file', kwargs={'pk': self.pk})
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_usage_type_display(self, standard=None):
|
def get_usage_type_display(self, standard=None):
|
||||||
"""
|
"""
|
||||||
Renvoie le libellé du type d'usage adapté dynamiquement à la norme régionale (BE, FR, INT).
|
Renvoie le libellé du type d'usage adapté dynamiquement à la norme régionale (BE, FR, INT).
|
||||||
|
|
@ -305,44 +166,6 @@ class PublicBuilding(AbstractLocation):
|
||||||
return f"Administratif ({terms['admin_building']})"
|
return f"Administratif ({terms['admin_building']})"
|
||||||
return dict(USAGE_TYPE_CHOICES).get(self.usage_type, self.usage_type)
|
return dict(USAGE_TYPE_CHOICES).get(self.usage_type, self.usage_type)
|
||||||
|
|
||||||
def get_icon_class(self):
|
|
||||||
"""
|
|
||||||
Retourne la classe d'icône Bootstrap adaptée au type de localisation.
|
|
||||||
"""
|
|
||||||
mapping = {
|
|
||||||
'site': 'bi-diagram-3',
|
|
||||||
'building': 'bi-building',
|
|
||||||
'wing': 'bi-building-add',
|
|
||||||
'level': 'bi-layers',
|
|
||||||
'basement': 'bi-layer-backward',
|
|
||||||
'attic': 'bi-triangle',
|
|
||||||
'mezzanine': 'bi-border-inner',
|
|
||||||
'stairwell': 'bi-stairs',
|
|
||||||
'corridor': 'bi-arrow-left-right',
|
|
||||||
'lobby': 'bi-door-open-fill',
|
|
||||||
'emergency_exit': 'bi-box-arrow-right',
|
|
||||||
'elevator_shaft': 'bi-arrow-down-up',
|
|
||||||
'technical_room': 'bi-tools',
|
|
||||||
'boiler_room': 'bi-fire',
|
|
||||||
'electrical_room': 'bi-lightning-charge',
|
|
||||||
'ventilation_room': 'bi-fan',
|
|
||||||
'elevator_machine_room': 'bi-gear-wide-connected',
|
|
||||||
'server_room': 'bi-hdd-network',
|
|
||||||
'room': 'bi-door-open',
|
|
||||||
'office': 'bi-briefcase',
|
|
||||||
'meeting_room': 'bi-people',
|
|
||||||
'classroom': 'bi-mortarboard',
|
|
||||||
'workshop': 'bi-hammer',
|
|
||||||
'sanitary_room': 'bi-badge-wc',
|
|
||||||
'storage_room': 'bi-archive',
|
|
||||||
'canteen_kitchen': 'bi-cup-hot',
|
|
||||||
'cleaning_room': 'bi-bucket',
|
|
||||||
'waste_room': 'bi-trash',
|
|
||||||
'indoor_parking': 'bi-car-front',
|
|
||||||
'outdoor_area': 'bi-tree',
|
|
||||||
}
|
|
||||||
return mapping.get(self.location_type, 'bi-geo-alt')
|
|
||||||
|
|
||||||
def get_child_locations(self):
|
def get_child_locations(self):
|
||||||
return self.child_locations.all()
|
return self.child_locations.all()
|
||||||
|
|
||||||
|
|
@ -379,36 +202,14 @@ class PublicBuilding(AbstractLocation):
|
||||||
class BuildingAsset(AbstractGeoAsset):
|
class BuildingAsset(AbstractGeoAsset):
|
||||||
"""
|
"""
|
||||||
Modèle représentant un asset ou équipement physique du bâtiment (Génie civil / Bâti, Façades, Toitures, CVC, Ascenseurs...).
|
Modèle représentant un asset ou équipement physique du bâtiment (Génie civil / Bâti, Façades, Toitures, CVC, Ascenseurs...).
|
||||||
Les assets peuvent former une arborescence parent-enfant (ex: Bâti -> Façade -> Châssis, ou Chaudière -> Brûleur).
|
Les assets peuvent former une arborescence parent-enfant (ex: Génie civil -> Façade Nord, Toiture, Ascenseur).
|
||||||
"""
|
"""
|
||||||
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="all_building_assets", verbose_name=_("Bâtiment / Site principal"))
|
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="all_building_assets", verbose_name=_("Bâtiment / Site principal"))
|
||||||
location = models.ForeignKey(PublicBuilding, on_delete=models.SET_NULL, null=True, blank=True, related_name="location_assets", verbose_name=_("Localisation spécifique"))
|
location = models.ForeignKey(PublicBuilding, on_delete=models.SET_NULL, null=True, blank=True, related_name="location_assets", verbose_name=_("Localisation spécifique"))
|
||||||
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets", verbose_name=_("Asset parent"))
|
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets", verbose_name=_("Asset parent"))
|
||||||
asset_type = models.CharField(max_length=50, choices=BUILDING_ASSET_TYPE_CHOICES, default='civil_engineering', verbose_name=_("Type d'asset / Équipement"))
|
asset_type = models.CharField(max_length=50, choices=BUILDING_ASSET_TYPE_CHOICES, default='civil_engineering', verbose_name=_("Type d'asset / Équipement"))
|
||||||
|
|
||||||
# Caractéristiques physiques & dimensionnelles
|
|
||||||
dimensions = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Dimensions / Format"))
|
|
||||||
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface unitaire (m²)"))
|
|
||||||
material = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Matériau principal"))
|
|
||||||
|
|
||||||
# Caractéristiques thermiques, énergétiques & techniques
|
|
||||||
power_kw = models.FloatField(null=True, blank=True, verbose_name=_("Puissance nominale (kW)"))
|
|
||||||
energy_source = models.CharField(max_length=50, choices=ENERGY_SOURCE_CHOICES, null=True, blank=True, verbose_name=_("Source d'énergie"))
|
|
||||||
refrigerant_type = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Fluide frigorigène"))
|
|
||||||
refrigerant_charge_kg = models.FloatField(null=True, blank=True, verbose_name=_("Charge fluide frigorigène (kg)"))
|
|
||||||
|
|
||||||
# Menuiseries, ouvrants & protection incendie
|
|
||||||
glazing_type = models.CharField(max_length=50, choices=GLAZING_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Type de vitrage"))
|
|
||||||
fire_rating = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Classement au feu / Coupe-feu"))
|
|
||||||
opening_type = models.CharField(max_length=50, choices=OPENING_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Type d'ouverture"))
|
|
||||||
|
|
||||||
# Maintenance, exploitation & suivi réglementaire
|
|
||||||
maintenance_contract_ref = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Réf. contrat maintenance"))
|
|
||||||
contractor_name = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Prestataire / Mainteneur"))
|
|
||||||
last_control_date = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle réglementaire"))
|
last_control_date = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle réglementaire"))
|
||||||
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle réglementaire"))
|
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle réglementaire"))
|
||||||
regulatory_periodicity_months = models.IntegerField(null=True, blank=True, verbose_name=_("Périodicité contrôle réglementaire (mois)"))
|
|
||||||
replacement_cost_estimate = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True, verbose_name=_("Coût estimé de renouvellement (€)"))
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
@ -439,266 +240,12 @@ class BuildingAsset(AbstractGeoAsset):
|
||||||
verbose_name_plural = _("Assets / Équipements de bâtiment")
|
verbose_name_plural = _("Assets / Équipements de bâtiment")
|
||||||
ordering = ['building', 'code']
|
ordering = ['building', 'code']
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.name_fr or self.name_nl or self.code
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingasset', 'asset_id': self.pk})
|
return reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingasset', 'asset_id': self.pk})
|
||||||
|
|
||||||
def get_child_assets(self):
|
def get_child_assets(self):
|
||||||
return self.child_assets.all()
|
return self.child_assets.all()
|
||||||
|
|
||||||
def get_macro_lot(self):
|
|
||||||
"""
|
|
||||||
Retourne le macro-lot / domaine technique de l'équipement.
|
|
||||||
"""
|
|
||||||
lots = {
|
|
||||||
# Clos & Couvert
|
|
||||||
'roof': _('Clos & Couvert'),
|
|
||||||
'roof_drainage': _('Clos & Couvert'),
|
|
||||||
'facade': _('Clos & Couvert'),
|
|
||||||
'window': _('Clos & Couvert'),
|
|
||||||
'skylight': _('Clos & Couvert'),
|
|
||||||
'external_door': _('Clos & Couvert'),
|
|
||||||
'shutter_blind': _('Clos & Couvert'),
|
|
||||||
'insulation': _('Clos & Couvert'),
|
|
||||||
# Aménagements & Second œuvre
|
|
||||||
'wall': _('Aménagements & Second œuvre'),
|
|
||||||
'interior_door': _('Aménagements & Second œuvre'),
|
|
||||||
'fire_door': _('Aménagements & Second œuvre'),
|
|
||||||
'ceiling': _('Aménagements & Second œuvre'),
|
|
||||||
'flooring': _('Aménagements & Second œuvre'),
|
|
||||||
'stairs': _('Aménagements & Second œuvre'),
|
|
||||||
# Génie climatique & CVC
|
|
||||||
'boiler': _('Génie climatique & CVC'),
|
|
||||||
'heat_pump': _('Génie climatique & CVC'),
|
|
||||||
'heat_substation': _('Génie climatique & CVC'),
|
|
||||||
'radiator_emitter': _('Génie climatique & CVC'),
|
|
||||||
'air_handling_unit': _('Génie climatique & CVC'),
|
|
||||||
'air_conditioning': _('Génie climatique & CVC'),
|
|
||||||
'water_heater': _('Génie climatique & CVC'),
|
|
||||||
'hvac': _('Génie climatique & CVC'),
|
|
||||||
# Électricité & Énergie
|
|
||||||
'electricity': _('Électricité & Énergie'),
|
|
||||||
'inverter_generator': _('Électricité & Énergie'),
|
|
||||||
'lighting': _('Électricité & Éclairage'),
|
|
||||||
'emergency_lighting': _('Électricité & Sécurité'),
|
|
||||||
'meter': _('Fluides & Comptage'),
|
|
||||||
'solar_panel': _('Énergies renouvelables'),
|
|
||||||
'ev_charging': _('Mobilité électrique'),
|
|
||||||
# Plomberie & Sanitaires
|
|
||||||
'sanitary': _('Plomberie & Sanitaires'),
|
|
||||||
'plumbing_piping': _('Plomberie & Réseaux'),
|
|
||||||
'sump_pump': _('Assainissement & Relevage'),
|
|
||||||
'rainwater_harvesting': _('Gestion des eaux pluviales'),
|
|
||||||
# Sécurité incendie & Sûreté
|
|
||||||
'fire_safety': _('Sécurité incendie'),
|
|
||||||
'smoke_detector': _('Sécurité incendie'),
|
|
||||||
'fire_extinguisher': _('Sécurité incendie'),
|
|
||||||
'smoke_exhaust': _('Sécurité incendie'),
|
|
||||||
'access_control': _('Sûreté & Contrôle d\'accès'),
|
|
||||||
'alarm': _('Sûreté & Contrôle d\'accès'),
|
|
||||||
'cctv_surveillance': _('Vidéosurveillance'),
|
|
||||||
# Mobilité verticale
|
|
||||||
'elevator': _('Ascenseurs & Élévateurs'),
|
|
||||||
'disabled_lift': _('Ascenseurs & Élévateurs'),
|
|
||||||
# Réseaux & Numérique
|
|
||||||
'network_rack': _('Réseaux & Télécom'),
|
|
||||||
'bms_system': _('Gestion Technique du Bâtiment (GTB)'),
|
|
||||||
# Gros œuvre & Divers
|
|
||||||
'civil_engineering': _('Génie civil & Structure'),
|
|
||||||
'other': _('Autre équipement'),
|
|
||||||
}
|
|
||||||
return lots.get(self.asset_type, _('Autre'))
|
|
||||||
|
|
||||||
def get_icon_class(self):
|
|
||||||
"""
|
|
||||||
Retourne l'icône Bootstrap adaptée au type d'asset.
|
|
||||||
"""
|
|
||||||
icons = {
|
|
||||||
'roof': 'bi-house-gear',
|
|
||||||
'roof_drainage': 'bi-water',
|
|
||||||
'facade': 'bi-border-style',
|
|
||||||
'window': 'bi-window',
|
|
||||||
'skylight': 'bi-sun',
|
|
||||||
'external_door': 'bi-door-closed',
|
|
||||||
'shutter_blind': 'bi-blinds',
|
|
||||||
'insulation': 'bi-shield',
|
|
||||||
'wall': 'bi-bricks',
|
|
||||||
'interior_door': 'bi-door-open',
|
|
||||||
'fire_door': 'bi-shield-shaded',
|
|
||||||
'ceiling': 'bi-grid-3x3',
|
|
||||||
'flooring': 'bi-square',
|
|
||||||
'stairs': 'bi-stairs',
|
|
||||||
'boiler': 'bi-fire',
|
|
||||||
'heat_pump': 'bi-snow',
|
|
||||||
'heat_substation': 'bi-shuffle',
|
|
||||||
'radiator_emitter': 'bi-thermometer-high',
|
|
||||||
'air_handling_unit': 'bi-fan',
|
|
||||||
'air_conditioning': 'bi-wind',
|
|
||||||
'water_heater': 'bi-droplet-half',
|
|
||||||
'hvac': 'bi-thermometer-half',
|
|
||||||
'electricity': 'bi-lightning-charge',
|
|
||||||
'inverter_generator': 'bi-battery-charging',
|
|
||||||
'lighting': 'bi-lightbulb',
|
|
||||||
'emergency_lighting': 'bi-exclamation-triangle',
|
|
||||||
'meter': 'bi-speedometer2',
|
|
||||||
'solar_panel': 'bi-sun-fill',
|
|
||||||
'ev_charging': 'bi-ev-station',
|
|
||||||
'sanitary': 'bi-badge-wc',
|
|
||||||
'plumbing_piping': 'bi-pipette',
|
|
||||||
'sump_pump': 'bi-arrow-up-circle',
|
|
||||||
'rainwater_harvesting': 'bi-cloud-rain',
|
|
||||||
'fire_safety': 'bi-shield-shaded',
|
|
||||||
'smoke_detector': 'bi-bell',
|
|
||||||
'fire_extinguisher': 'bi-cone-striped',
|
|
||||||
'smoke_exhaust': 'bi-cloud-arrow-up',
|
|
||||||
'access_control': 'bi-key',
|
|
||||||
'alarm': 'bi-shield-exclamation',
|
|
||||||
'cctv_surveillance': 'bi-camera-video',
|
|
||||||
'elevator': 'bi-arrow-down-up',
|
|
||||||
'disabled_lift': 'bi-person-wheelchair',
|
|
||||||
'network_rack': 'bi-hdd-rack',
|
|
||||||
'bms_system': 'bi-cpu',
|
|
||||||
'civil_engineering': 'bi-building-gear',
|
|
||||||
'other': 'bi-gear',
|
|
||||||
}
|
|
||||||
return icons.get(self.asset_type, 'bi-gear')
|
|
||||||
|
|
||||||
def get_asset_theme(self):
|
|
||||||
"""
|
|
||||||
Retourne la configuration graphique (couleurs d'icône et de badge)
|
|
||||||
selon la typologie métier de l'asset.
|
|
||||||
"""
|
|
||||||
# 1. Sécurité incendie (Rouge alerte)
|
|
||||||
if self.asset_type in ('fire_safety', 'smoke_detector', 'fire_extinguisher',
|
|
||||||
'smoke_exhaust', 'fire_door'):
|
|
||||||
return {
|
|
||||||
'color': '#b91c1c', # red-700
|
|
||||||
'bg_color': '#fef2f2', # red-50
|
|
||||||
'border_color': '#fecaca', # red-200
|
|
||||||
'icon_color': '#dc2626', # red-600
|
|
||||||
'badge_class': 'bg-danger-subtle text-danger border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2. Génie climatique & CVC (Orange thermique / Chaleur & Froid)
|
|
||||||
if self.asset_type in ('boiler', 'heat_pump', 'heat_substation', 'radiator_emitter',
|
|
||||||
'air_handling_unit', 'air_conditioning', 'water_heater', 'hvac'):
|
|
||||||
return {
|
|
||||||
'color': '#c2410c', # orange-700
|
|
||||||
'bg_color': '#fff7ed', # orange-50
|
|
||||||
'border_color': '#fed7aa', # orange-200
|
|
||||||
'icon_color': '#ea580c', # orange-600
|
|
||||||
'badge_class': 'bg-warning-subtle text-danger-emphasis border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 3. Électricité, Énergie & Éclairage (Ambre / Jaune or)
|
|
||||||
if self.asset_type in ('electricity', 'inverter_generator', 'lighting', 'emergency_lighting',
|
|
||||||
'meter', 'solar_panel', 'ev_charging'):
|
|
||||||
return {
|
|
||||||
'color': '#a16207', # yellow-700
|
|
||||||
'bg_color': '#fefce8', # yellow-50
|
|
||||||
'border_color': '#fef08a', # yellow-200
|
|
||||||
'icon_color': '#ca8a04', # yellow-600
|
|
||||||
'badge_class': 'bg-warning-subtle text-dark border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 4. Sûreté, Contrôle d'accès & Vidéosurveillance (Pourpre / Violet)
|
|
||||||
if self.asset_type in ('access_control', 'alarm', 'cctv_surveillance'):
|
|
||||||
return {
|
|
||||||
'color': '#7e22ce', # purple-700
|
|
||||||
'bg_color': '#faf5ff', # purple-50
|
|
||||||
'border_color': '#e9d5ff', # purple-200
|
|
||||||
'icon_color': '#9333ea', # purple-600
|
|
||||||
'badge_class': 'bg-primary-subtle text-primary border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 5. Clos & Couvert / Enveloppe (Teal / Vert canard)
|
|
||||||
if self.asset_type in ('roof', 'roof_drainage', 'facade', 'window', 'skylight',
|
|
||||||
'external_door', 'shutter_blind', 'insulation'):
|
|
||||||
return {
|
|
||||||
'color': '#0f766e', # teal-700
|
|
||||||
'bg_color': '#f0fdfa', # teal-50
|
|
||||||
'border_color': '#99f6e4', # teal-200
|
|
||||||
'icon_color': '#0d9488', # teal-600
|
|
||||||
'badge_class': 'bg-success-subtle text-success border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 6. Plomberie, Sanitaires & Gestion de l'eau (Bleu azur / Sky)
|
|
||||||
if self.asset_type in ('sanitary', 'plumbing_piping', 'sump_pump', 'rainwater_harvesting'):
|
|
||||||
return {
|
|
||||||
'color': '#0369a1', # sky-700
|
|
||||||
'bg_color': '#f0f9ff', # sky-50
|
|
||||||
'border_color': '#bae6fd', # sky-200
|
|
||||||
'icon_color': '#0284c7', # sky-600
|
|
||||||
'badge_class': 'bg-info-subtle text-info border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7. Mobilité verticale / Ascenseurs (Bleu cobalt)
|
|
||||||
if self.asset_type in ('elevator', 'disabled_lift'):
|
|
||||||
return {
|
|
||||||
'color': '#1d4ed8', # blue-700
|
|
||||||
'bg_color': '#eff6ff', # blue-50
|
|
||||||
'border_color': '#bfdbfe', # blue-200
|
|
||||||
'icon_color': '#2563eb', # blue-600
|
|
||||||
'badge_class': 'bg-primary-subtle text-primary border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 8. Réseaux & Numérique / GTB (Indigo informatique)
|
|
||||||
if self.asset_type in ('network_rack', 'bms_system'):
|
|
||||||
return {
|
|
||||||
'color': '#4338ca', # indigo-700
|
|
||||||
'bg_color': '#eef2ff', # indigo-50
|
|
||||||
'border_color': '#c7d2fe', # indigo-200
|
|
||||||
'icon_color': '#4f46e5', # indigo-600
|
|
||||||
'badge_class': 'bg-dark-subtle text-dark border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 9. Second œuvre & Aménagements intérieurs (Ardoise neutre)
|
|
||||||
if self.asset_type in ('wall', 'interior_door', 'ceiling', 'flooring', 'stairs'):
|
|
||||||
return {
|
|
||||||
'color': '#334155', # slate-700
|
|
||||||
'bg_color': '#f8fafc', # slate-50
|
|
||||||
'border_color': '#cbd5e1', # slate-300
|
|
||||||
'icon_color': '#64748b', # slate-500
|
|
||||||
'badge_class': 'bg-secondary-subtle text-secondary border',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Par défaut (Gros œuvre & Autre)
|
|
||||||
return {
|
|
||||||
'color': '#475569',
|
|
||||||
'bg_color': '#f1f5f9',
|
|
||||||
'border_color': '#cbd5e1',
|
|
||||||
'icon_color': '#64748b',
|
|
||||||
'badge_class': 'bg-light text-muted border',
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def theme_color(self):
|
|
||||||
return self.get_asset_theme()['color']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def theme_icon_color(self):
|
|
||||||
return self.get_asset_theme()['icon_color']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def theme_bg_color(self):
|
|
||||||
return self.get_asset_theme()['bg_color']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def theme_border_color(self):
|
|
||||||
return self.get_asset_theme()['border_color']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def theme_badge_style(self):
|
|
||||||
t = self.get_asset_theme()
|
|
||||||
return f"background-color: {t['bg_color']} !important; color: {t['color']} !important; border: 1px solid {t['border_color']} !important;"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def theme_badge_class(self):
|
|
||||||
return self.get_asset_theme()['badge_class']
|
|
||||||
|
|
||||||
|
|
||||||
# Backward compatibility alias
|
# Backward compatibility alias
|
||||||
BuildingEquipment = BuildingAsset
|
BuildingEquipment = BuildingAsset
|
||||||
|
|
@ -708,34 +255,14 @@ def upload_to_building(instance, filename):
|
||||||
return f"buildings/{instance.building.code}/{filename}"
|
return f"buildings/{instance.building.code}/{filename}"
|
||||||
|
|
||||||
|
|
||||||
BUILDING_DOCUMENT_TYPE_CHOICES = (
|
|
||||||
('floor_plan', _("Plan d'étage / Plan architectural")),
|
|
||||||
('as_built', _("Plan de récolement (As-Built)")),
|
|
||||||
('erp_notice', _("Notice de sécurité / Dossier ERP")),
|
|
||||||
('dpe', _("Diagnostic énergétique (DPE / PEB)")),
|
|
||||||
('technical_notice', _("Notice technique / Manuel d'exploitation")),
|
|
||||||
('other', _("Autre document")),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PublicBuildingDocument(Document):
|
class PublicBuildingDocument(Document):
|
||||||
"""
|
"""
|
||||||
Documents associés à un bâtiment public (plans, notices ERP, DPE).
|
Documents associés à un bâtiment public (plans, notices ERP, DPE).
|
||||||
"""
|
"""
|
||||||
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="documents", verbose_name=_("Bâtiment"))
|
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="documents", verbose_name=_("Bâtiment"))
|
||||||
floor_location = models.ForeignKey(
|
|
||||||
PublicBuilding,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="floor_documents",
|
|
||||||
verbose_name=_("Niveau / Étage concerné"),
|
|
||||||
help_text=_("Niveau ou local auquel ce document/plan se rapporte.")
|
|
||||||
)
|
|
||||||
file = models.FileField(upload_to=upload_to_building, storage=private_storage, verbose_name=_("Fichier"), max_length=255)
|
file = models.FileField(upload_to=upload_to_building, storage=private_storage, verbose_name=_("Fichier"), max_length=255)
|
||||||
document_type = models.CharField(max_length=50, choices=BUILDING_DOCUMENT_TYPE_CHOICES, default='other', verbose_name=_("Type de document"))
|
document_type = models.CharField(max_length=50, default='other', verbose_name=_("Type de document"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Document de bâtiment")
|
verbose_name = _("Document de bâtiment")
|
||||||
verbose_name_plural = _("Documents de bâtiment")
|
verbose_name_plural = _("Documents de bâtiment")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import get_language, gettext_lazy as _
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
@ -91,7 +91,6 @@ class ITSGeoAsset(AbstractGeoAsset):
|
||||||
location = models.ForeignKey(ITSLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True)
|
location = models.ForeignKey(ITSLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
trafficlights_pole = models.ForeignKey("assets.TrafficLightPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Traffic Light Pole")
|
trafficlights_pole = models.ForeignKey("assets.TrafficLightPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Traffic Light Pole")
|
||||||
lighting_pole = models.ForeignKey("assets.PublicLightingPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Lighting Pole")
|
lighting_pole = models.ForeignKey("assets.PublicLightingPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Lighting Pole")
|
||||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
@ -120,155 +119,6 @@ class ITSGeoAsset(AbstractGeoAsset):
|
||||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsgeoasset', 'asset_id':self.id})
|
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsgeoasset', 'asset_id':self.id})
|
||||||
|
|
||||||
|
|
||||||
BOLLARD_DIRECTION_CHOICES = (
|
|
||||||
('unidirectional', _('Sens unique')),
|
|
||||||
('bidirectional', _('Double sens')),
|
|
||||||
)
|
|
||||||
|
|
||||||
class ITSBollardSystem(AbstractGeoAsset):
|
|
||||||
"""
|
|
||||||
Système / Traverse de bornes escamotables (représentation linéaire).
|
|
||||||
Contrôle l'accès à une voie ou zone piétonne.
|
|
||||||
"""
|
|
||||||
geom = models.LineStringField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie (Ligne)"))
|
|
||||||
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Modèle de système"))
|
|
||||||
location = models.ForeignKey(ITSLocation, related_name="bollard_systems", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Localisation ITS"))
|
|
||||||
width = models.FloatField(null=True, blank=True, verbose_name=_("Largeur de passage (m)"))
|
|
||||||
direction = models.CharField(max_length=50, choices=BOLLARD_DIRECTION_CHOICES, default='bidirectional', null=True, blank=True, verbose_name=_("Sens de circulation"))
|
|
||||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
|
||||||
associated_assets = models.ManyToManyField(
|
|
||||||
'assets.ITSGeoAsset',
|
|
||||||
through='assets.ITSBollardSystemAsset',
|
|
||||||
related_name="bollard_systems",
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Équipements associés")
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Système de bornes")
|
|
||||||
verbose_name_plural = _("Systèmes de bornes")
|
|
||||||
indexes = [
|
|
||||||
models.Index(fields=["code"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsbollardsystem', 'asset_id': self.id})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def plots_count(self):
|
|
||||||
return self.plots.count()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.get_name()
|
|
||||||
|
|
||||||
|
|
||||||
BOLLARD_TYPE_CHOICES = (
|
|
||||||
('automatic', _('Automatique / Escamotable')),
|
|
||||||
('semi_automatic', _('Semi-automatique')),
|
|
||||||
('fixed', _('Fixe')),
|
|
||||||
('removable', _('Amovible')),
|
|
||||||
)
|
|
||||||
|
|
||||||
class ITSBollardPlot(AbstractGeoAsset):
|
|
||||||
"""
|
|
||||||
Plot / borne physique individuel sur un système de bornes.
|
|
||||||
"""
|
|
||||||
geom = models.PointField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie (Point)"))
|
|
||||||
system = models.ForeignKey(ITSBollardSystem, on_delete=models.CASCADE, related_name="plots", verbose_name=_("Système de bornes"))
|
|
||||||
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Modèle de plot"))
|
|
||||||
position = models.PositiveSmallIntegerField(default=1, verbose_name=_("Position sur la traverse"), help_text=_("Ordre de gauche à droite (1, 2, 3...)"))
|
|
||||||
bollard_type = models.CharField(max_length=50, choices=BOLLARD_TYPE_CHOICES, default='automatic', verbose_name=_("Type de plot"))
|
|
||||||
location = models.ForeignKey(ITSLocation, related_name="bollard_plots", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Localisation ITS"))
|
|
||||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.get_name()
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
if not self.location_id and self.system and self.system.location_id:
|
|
||||||
self.location = self.system.location
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if not self.location_id and self.system and self.system.location_id:
|
|
||||||
self.location = self.system.location
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Plot de borne")
|
|
||||||
verbose_name_plural = _("Plots de bornes")
|
|
||||||
ordering = ["system", "position"]
|
|
||||||
indexes = [
|
|
||||||
models.Index(fields=["code"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsbollardplot', 'asset_id': self.id})
|
|
||||||
|
|
||||||
|
|
||||||
class ITSBollardSystemAsset(models.Model):
|
|
||||||
"""
|
|
||||||
Table de liaison ManyToMany entre un ITSBollardSystem et des ITSGeoAsset
|
|
||||||
(caméras LAPI, totems, armoires électriques, feux bicolores, boucles...).
|
|
||||||
"""
|
|
||||||
class AssetRole(models.TextChoices):
|
|
||||||
CAMERA_ENTRY = "camera_entry", _("Caméra entrée (LAPI)")
|
|
||||||
CAMERA_EXIT = "camera_exit", _("Caméra sortie (LAPI)")
|
|
||||||
CAMERA_CCTV = "camera_cctv", _("Caméra de contexte / surveillance")
|
|
||||||
CABINET = "cabinet", _("Armoire électrique / automate")
|
|
||||||
TOTEM = "totem", _("Totem de commande")
|
|
||||||
TRAFFIC_LIGHT = "traffic_light", _("Feu de franchissement")
|
|
||||||
LOOP = "loop", _("Boucle de détection")
|
|
||||||
OTHER = "other", _("Autre")
|
|
||||||
|
|
||||||
bollard_system = models.ForeignKey(
|
|
||||||
ITSBollardSystem,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="system_assets",
|
|
||||||
verbose_name=_("Système de bornes")
|
|
||||||
)
|
|
||||||
asset = models.ForeignKey(
|
|
||||||
ITSGeoAsset,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="bollard_system_links",
|
|
||||||
verbose_name=_("Équipement ITS")
|
|
||||||
)
|
|
||||||
role = models.CharField(
|
|
||||||
max_length=50,
|
|
||||||
choices=AssetRole.choices,
|
|
||||||
default=AssetRole.OTHER,
|
|
||||||
verbose_name=_("Rôle de l'équipement")
|
|
||||||
)
|
|
||||||
comment = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Commentaire"))
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date d'association"))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Équipement associé au système de bornes")
|
|
||||||
verbose_name_plural = _("Équipements associés au système de bornes")
|
|
||||||
unique_together = ('bollard_system', 'asset')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.bollard_system.code} - {self.asset.code} ({self.get_role_display()})"
|
|
||||||
|
|
||||||
|
|
||||||
class ITSBollardSystemContract(models.Model):
|
|
||||||
system = models.ForeignKey(ITSBollardSystem, on_delete=models.CASCADE, related_name="bollard_systems_contracts")
|
|
||||||
contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="bollard_systems_contracts")
|
|
||||||
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
|
|
||||||
maintenance_type = models.CharField(max_length=50, choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.system.code} - {self.contract.contract_number}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Contrat de système de bornes")
|
|
||||||
verbose_name_plural = _("Contrats de systèmes de bornes")
|
|
||||||
unique_together = ('system', 'contract')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ITSLocationContract(models.Model):
|
class ITSLocationContract(models.Model):
|
||||||
location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="its_locations_contracts")
|
location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="its_locations_contracts")
|
||||||
|
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
from django.contrib.gis.db import models
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import gettext_lazy as _, get_language
|
|
||||||
import json
|
|
||||||
|
|
||||||
from common.models import Thematic, Document
|
|
||||||
from common.storages import PrivateMediaStorage
|
|
||||||
|
|
||||||
private_storage = PrivateMediaStorage()
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneType(models.Model):
|
|
||||||
"""
|
|
||||||
Typologie de zone de gestion spécifique à une thématique
|
|
||||||
(ex: Fauche tardive / refuge biodiversité, Tonte d'agrément, Onde verte coordonnée, etc.).
|
|
||||||
"""
|
|
||||||
code = models.CharField(max_length=100, unique=True, verbose_name=_("Code"))
|
|
||||||
name_fr = models.CharField(max_length=255, verbose_name=_("Nom (FR)"))
|
|
||||||
name_nl = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Nom (NL)"))
|
|
||||||
thematic = models.ForeignKey(
|
|
||||||
Thematic,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="management_zone_types",
|
|
||||||
verbose_name=_("Thématique")
|
|
||||||
)
|
|
||||||
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
|
|
||||||
default_color = models.CharField(max_length=20, default="#198754", verbose_name=_("Couleur par défaut"))
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
lang = get_language()
|
|
||||||
if lang and lang.startswith("nl") and self.name_nl:
|
|
||||||
return self.name_nl
|
|
||||||
return self.name_fr
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.get_name()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.get_name()} ({self.thematic.name})"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["thematic", "name_fr"]
|
|
||||||
verbose_name = _("Type de zone de gestion")
|
|
||||||
verbose_name_plural = _("Types de zones de gestion")
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZone(models.Model):
|
|
||||||
"""
|
|
||||||
Zone de gestion générique avec géométrie polygonale.
|
|
||||||
Regroupe des équipements/assets selon une logique métier, d'exploitation ou de gestion écologique.
|
|
||||||
"""
|
|
||||||
code = models.CharField(max_length=100, unique=True, verbose_name=_("Code"))
|
|
||||||
name_fr = models.CharField(max_length=255, verbose_name=_("Nom (FR)"))
|
|
||||||
name_nl = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Nom (NL)"))
|
|
||||||
thematic = models.ForeignKey(
|
|
||||||
Thematic,
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name="management_zones",
|
|
||||||
verbose_name=_("Thématique")
|
|
||||||
)
|
|
||||||
zone_type = models.ForeignKey(
|
|
||||||
ManagementZoneType,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="management_zones",
|
|
||||||
verbose_name=_("Type de zone de gestion")
|
|
||||||
)
|
|
||||||
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
|
|
||||||
instructions = models.TextField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name=_("Consignes et points d'attention"),
|
|
||||||
help_text=_("Consignes d'exploitation, points de vigilance, règles de sécurité...")
|
|
||||||
)
|
|
||||||
geom = models.MultiPolygonField(
|
|
||||||
srid=3812,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Périmètre géographique")
|
|
||||||
)
|
|
||||||
color = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name=_("Couleur d'affichage"),
|
|
||||||
help_text=_("Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone.")
|
|
||||||
)
|
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
|
||||||
target_frequency_days = models.IntegerField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Fréquence cible (jours)"),
|
|
||||||
help_text=_("Intervalle indicatif entre interventions en jours.")
|
|
||||||
)
|
|
||||||
seasonality_notes = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name=_("Saisonnalité / Calendrier"),
|
|
||||||
help_text=_("Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps.")
|
|
||||||
)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date de création"))
|
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Date de mise à jour"))
|
|
||||||
created_by = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="created_management_zones",
|
|
||||||
verbose_name=_("Créé par")
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_color(self):
|
|
||||||
if self.color:
|
|
||||||
return self.color
|
|
||||||
if self.zone_type and self.zone_type.default_color:
|
|
||||||
return self.zone_type.default_color
|
|
||||||
return "#198754"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def geojson_geometry(self):
|
|
||||||
"""
|
|
||||||
Retourne la géométrie GeoJSON transformée en WGS84 (EPSG:4326)
|
|
||||||
pour utilisation directe dans MapLibre / Leaflet.
|
|
||||||
"""
|
|
||||||
if not self.geom:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
geom_4326 = self.geom.transform(4326, clone=True)
|
|
||||||
return json.loads(geom_4326.geojson)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
lang = get_language()
|
|
||||||
if lang and lang.startswith("nl") and self.name_nl:
|
|
||||||
return self.name_nl
|
|
||||||
return self.name_fr
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.get_name()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.code} - {self.get_name()}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["thematic", "code"]
|
|
||||||
verbose_name = _("Zone de gestion")
|
|
||||||
verbose_name_plural = _("Zones de gestion")
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneAsset(models.Model):
|
|
||||||
"""
|
|
||||||
Liaison explicite (inclusion ou exclusion) d'un asset à une zone de gestion.
|
|
||||||
Permet d'affiner le rattachement au-delà de la stricte inclusion spatiale.
|
|
||||||
"""
|
|
||||||
management_zone = models.ForeignKey(
|
|
||||||
ManagementZone,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="explicit_assets",
|
|
||||||
verbose_name=_("Zone de gestion")
|
|
||||||
)
|
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_("Type d'asset"))
|
|
||||||
object_id = models.PositiveIntegerField(verbose_name=_("ID de l'asset"))
|
|
||||||
asset = GenericForeignKey('content_type', 'object_id')
|
|
||||||
is_excluded = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
verbose_name=_("Exclu"),
|
|
||||||
help_text=_("Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre.")
|
|
||||||
)
|
|
||||||
notes = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Notes"))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
status = "Exclu" if self.is_excluded else "Inclus"
|
|
||||||
return f"{self.management_zone.code} -> {self.content_type.model} #{self.object_id} ({status})"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ('management_zone', 'content_type', 'object_id')
|
|
||||||
verbose_name = _("Asset de zone de gestion")
|
|
||||||
verbose_name_plural = _("Assets de zones de gestion")
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneDocument(Document):
|
|
||||||
"""
|
|
||||||
Documents, fiches techniques et arrêtés associés à une zone de gestion.
|
|
||||||
"""
|
|
||||||
zone = models.ForeignKey(
|
|
||||||
ManagementZone,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="documents",
|
|
||||||
verbose_name=_("Zone de gestion")
|
|
||||||
)
|
|
||||||
file = models.FileField(
|
|
||||||
upload_to="assets_documents/management_zones/",
|
|
||||||
storage=private_storage,
|
|
||||||
verbose_name=_("Fichier"),
|
|
||||||
max_length=255
|
|
||||||
)
|
|
||||||
thumbnail = models.ImageField(
|
|
||||||
upload_to="assets_documents/management_zones/",
|
|
||||||
storage=private_storage,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
max_length=255
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Document de zone de gestion")
|
|
||||||
verbose_name_plural = _("Documents de zones de gestion")
|
|
||||||
|
|
@ -146,13 +146,6 @@ TREE_PRUNING_CHOICES = [
|
||||||
('none', _("Non taillé / Entretien minimal")),
|
('none', _("Non taillé / Entretien minimal")),
|
||||||
]
|
]
|
||||||
|
|
||||||
TREE_MANAGEMENT_TYPE_CHOICES = [
|
|
||||||
('municipal', _("Communal (Régie communale)")),
|
|
||||||
('regional', _("Régional (Bruxelles Mobilité / Région)")),
|
|
||||||
('community', _("Intercommunal / Autre pouvoir public")),
|
|
||||||
('private', _("Privé / Tiers")),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
BULB_TYPE_CHOICES = [
|
BULB_TYPE_CHOICES = [
|
||||||
('tulipes', _('Tulipes')),
|
('tulipes', _('Tulipes')),
|
||||||
|
|
@ -231,37 +224,6 @@ class NatureTree(AbstractGeoAsset):
|
||||||
pruning_frequency_years = models.IntegerField(null=True, blank=True, verbose_name=_("Périodicité de taille (années)"))
|
pruning_frequency_years = models.IntegerField(null=True, blank=True, verbose_name=_("Périodicité de taille (années)"))
|
||||||
last_pruning_date = models.DateField(null=True, blank=True, verbose_name=_("Dernière taille / élagage"))
|
last_pruning_date = models.DateField(null=True, blank=True, verbose_name=_("Dernière taille / élagage"))
|
||||||
|
|
||||||
# Gestionnaire & Compétence
|
|
||||||
managing_authority = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Autorité gestionnaire"))
|
|
||||||
management_type = models.CharField(max_length=50, choices=TREE_MANAGEMENT_TYPE_CHOICES, default='municipal', null=True, blank=True, verbose_name=_("Type de gestionnaire"))
|
|
||||||
|
|
||||||
# Taxonomie & Botanique
|
|
||||||
vernacular_name = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Nom vernaculaire / Essence"))
|
|
||||||
scientific_name = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Nom scientifique / latin"))
|
|
||||||
genus = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Genre botanique"))
|
|
||||||
cultivar = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Cultivar / Variété"))
|
|
||||||
|
|
||||||
# Dendrométrie & Morphologie complémentaire
|
|
||||||
number_of_trunks = models.IntegerField(null=True, blank=True, default=1, verbose_name=_("Nombre de troncs / Cépée"))
|
|
||||||
|
|
||||||
# Éco-services & Valeur patrimoniale (Indicateurs écosystémiques)
|
|
||||||
amenity_value = models.FloatField(null=True, blank=True, verbose_name=_("Valeur d'aménité (€)"))
|
|
||||||
carbon_stock = models.FloatField(null=True, blank=True, verbose_name=_("Stock de carbone (t CO2)"))
|
|
||||||
cooling_indicator = models.FloatField(null=True, blank=True, verbose_name=_("Indice de rafraîchissement"))
|
|
||||||
cooling_energy_indicator = models.FloatField(null=True, blank=True, verbose_name=_("Énergie de refroidissement (kWh)"))
|
|
||||||
biodiversity_index = models.FloatField(null=True, blank=True, verbose_name=_("Indice de biodiversité"))
|
|
||||||
|
|
||||||
# Évaluation patrimoniale & Barème
|
|
||||||
situation_coefficient = models.FloatField(null=True, blank=True, verbose_name=_("Coefficient de situation"))
|
|
||||||
plantation_coefficient = models.FloatField(null=True, blank=True, verbose_name=_("Coefficient de plantation"))
|
|
||||||
|
|
||||||
# Statut biologique particulier
|
|
||||||
is_dead = models.BooleanField(default=False, verbose_name=_("Arbre mort"))
|
|
||||||
is_stump = models.BooleanField(default=False, verbose_name=_("Souche"))
|
|
||||||
is_chronoxyle = models.BooleanField(default=False, verbose_name=_("Arbre sénescent / Chronoxyle (bois mort sur pied)"))
|
|
||||||
death_date = models.DateField(null=True, blank=True, verbose_name=_("Date de dépérissement / mort"))
|
|
||||||
planting_date = models.DateField(null=True, blank=True, verbose_name=_("Date de plantation réelle"))
|
|
||||||
|
|
||||||
# Références des choix pour utilisation simple dans les templates
|
# Références des choix pour utilisation simple dans les templates
|
||||||
DEVELOPMENT_STAGE_CHOICES = TREE_DEVELOPMENT_STAGE_CHOICES
|
DEVELOPMENT_STAGE_CHOICES = TREE_DEVELOPMENT_STAGE_CHOICES
|
||||||
VITALITY_CHOICES = TREE_VITALITY_CHOICES
|
VITALITY_CHOICES = TREE_VITALITY_CHOICES
|
||||||
|
|
@ -271,7 +233,6 @@ class NatureTree(AbstractGeoAsset):
|
||||||
PIT_TYPE_CHOICES = TREE_PIT_TYPE_CHOICES
|
PIT_TYPE_CHOICES = TREE_PIT_TYPE_CHOICES
|
||||||
PROTECTION_CHOICES = TREE_PROTECTION_CHOICES
|
PROTECTION_CHOICES = TREE_PROTECTION_CHOICES
|
||||||
PRUNING_CHOICES = TREE_PRUNING_CHOICES
|
PRUNING_CHOICES = TREE_PRUNING_CHOICES
|
||||||
MANAGEMENT_TYPE_CHOICES = TREE_MANAGEMENT_TYPE_CHOICES
|
|
||||||
|
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["code"]),
|
models.Index(fields=["code"]),
|
||||||
|
|
@ -281,11 +242,6 @@ class NatureTree(AbstractGeoAsset):
|
||||||
verbose_name = _("Arbre")
|
verbose_name = _("Arbre")
|
||||||
verbose_name_plural = _("Arbres")
|
verbose_name_plural = _("Arbres")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
from assets.utils.tree_ecoservices import compute_tree_ecoservices
|
|
||||||
compute_tree_ecoservices(self, overwrite_existing=False)
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturetree', 'asset_id':self.id})
|
return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturetree', 'asset_id':self.id})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,13 @@ private_storage = PrivateMediaStorage()
|
||||||
|
|
||||||
|
|
||||||
STRUCTURE_TYPE_CHOICES = (
|
STRUCTURE_TYPE_CHOICES = (
|
||||||
('tunnel', _('Tunnel')),
|
('tunnel', 'Tunnnel'),
|
||||||
('bridge', _('Pont')),
|
('bridge', 'Pont'),
|
||||||
('viaduct', _('Viaduc')),
|
('viaduct', 'Viaduc'),
|
||||||
('overpass', _('Passerelle')),
|
('overpass', 'Passerelle'),
|
||||||
('underpass', _('Passage souterrain')),
|
('underpass', 'Passage souterrain'),
|
||||||
('gallery', _('Galerie technique')),
|
('gallery', 'Galerie technique'),
|
||||||
('atypical', _('Ouvrage atypique')),
|
('atypical', 'Ouvrage atypique'),
|
||||||
('metro_station', _('Station de métro')),
|
|
||||||
('metro_tunnel', _('Tunnel de métro')),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,32 +16,32 @@ ASSET_EDITABLE_FIELDS_BY_ROLE = {
|
||||||
'admin': [
|
'admin': [
|
||||||
'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category',
|
'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category',
|
||||||
'serial_number', 'installation_date', 'brand', 'warranty_duration',
|
'serial_number', 'installation_date', 'brand', 'warranty_duration',
|
||||||
'last_inspection_date', 'funding_program', 'note', 'geom', 'lon', 'lat',
|
'last_inspection_date', 'funding_program', 'geom', 'lon', 'lat',
|
||||||
'location_id', 'intersection_id',
|
'location_id', 'intersection_id',
|
||||||
],
|
],
|
||||||
'manager': [
|
'manager': [
|
||||||
'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category',
|
'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category',
|
||||||
'serial_number', 'installation_date', 'brand', 'warranty_duration',
|
'serial_number', 'installation_date', 'brand', 'warranty_duration',
|
||||||
'last_inspection_date', 'funding_program', 'note', 'geom', 'lon', 'lat',
|
'last_inspection_date', 'funding_program', 'geom', 'lon', 'lat',
|
||||||
'location_id', 'intersection_id',
|
'location_id', 'intersection_id',
|
||||||
],
|
],
|
||||||
'controller': [
|
'controller': [
|
||||||
'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category',
|
'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category',
|
||||||
'serial_number', 'installation_date', 'brand', 'warranty_duration',
|
'serial_number', 'installation_date', 'brand', 'warranty_duration',
|
||||||
'last_inspection_date', 'funding_program', 'note', 'geom', 'lon', 'lat',
|
'last_inspection_date', 'funding_program', 'geom', 'lon', 'lat',
|
||||||
'location_id', 'intersection_id',
|
'location_id', 'intersection_id',
|
||||||
],
|
],
|
||||||
'external_manager': [
|
'external_manager': [
|
||||||
'name_fr', 'name_nl', 'external_reference', 'category', 'brand', 'serial_number', 'funding_program', 'note',
|
'name_fr', 'name_nl', 'external_reference', 'category', 'brand', 'serial_number', 'funding_program'
|
||||||
],
|
],
|
||||||
'technician': ['name_fr', 'name_nl', 'external_reference', 'category', 'brand', 'serial_number', 'funding_program', 'note'],
|
'technician': ['name_fr', 'name_nl', 'external_reference', 'category', 'brand', 'serial_number', 'funding_program'],
|
||||||
'operator': [],
|
'operator': [],
|
||||||
'observer': [],
|
'observer': [],
|
||||||
'viewer': [],
|
'viewer': [],
|
||||||
'editor': [
|
'editor': [
|
||||||
'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category',
|
'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category',
|
||||||
'serial_number', 'installation_date', 'brand', 'warranty_duration',
|
'serial_number', 'installation_date', 'brand', 'warranty_duration',
|
||||||
'last_inspection_date', 'funding_program', 'note', 'geom', 'lon', 'lat',
|
'last_inspection_date', 'funding_program', 'geom', 'lon', 'lat',
|
||||||
'location_id', 'intersection_id',
|
'location_id', 'intersection_id',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -258,9 +258,9 @@ def can_view_asset(user, asset):
|
||||||
if not can_view:
|
if not can_view:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Vérifier la restriction par contrat et commune pour les utilisateurs externes ou limités aux contrats
|
# Vérifier la restriction par contrat pour les utilisateurs externes
|
||||||
is_external = (user_config is None) or (not user_config.is_intern)
|
is_external = (user_config is None) or (not user_config.is_intern)
|
||||||
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
must_limit = is_external and (not user_config or user_config.limit_assets_to_contracts)
|
||||||
if must_limit and user_config:
|
if must_limit and user_config:
|
||||||
asset_cids = get_asset_contract_ids(asset)
|
asset_cids = get_asset_contract_ids(asset)
|
||||||
if asset_cids is not None:
|
if asset_cids is not None:
|
||||||
|
|
@ -271,35 +271,8 @@ def can_view_asset(user, asset):
|
||||||
can_view_assets=True
|
can_view_assets=True
|
||||||
).values_list('contract_id', flat=True)
|
).values_list('contract_id', flat=True)
|
||||||
)
|
)
|
||||||
if user_config.default_contract_id:
|
|
||||||
accessible_cids.add(user_config.default_contract_id)
|
|
||||||
if not (asset_cids & accessible_cids):
|
if not (asset_cids & accessible_cids):
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
# Vérifier la restriction par commune pour les assets sans contrat direct
|
|
||||||
accessible_munis = get_user_accessible_municipalities(user)
|
|
||||||
accessible_muni_ids = set(accessible_munis.values_list('id', flat=True))
|
|
||||||
if accessible_muni_ids:
|
|
||||||
muni_id = getattr(asset, 'municipality_id', None)
|
|
||||||
if not muni_id and hasattr(asset, 'location') and asset.location:
|
|
||||||
muni_id = getattr(asset.location, 'municipality_id', None)
|
|
||||||
if not muni_id and hasattr(asset, 'street') and asset.street:
|
|
||||||
muni_id = getattr(asset.street, 'municipality_id', None)
|
|
||||||
if not muni_id and hasattr(asset, 'pole') and asset.pole:
|
|
||||||
muni_id = getattr(asset.pole, 'municipality_id', None)
|
|
||||||
if not muni_id and hasattr(asset.pole, 'street') and asset.pole.street:
|
|
||||||
muni_id = getattr(asset.pole.street, 'municipality_id', None)
|
|
||||||
if not muni_id and hasattr(asset, 'intersection') and asset.intersection:
|
|
||||||
muni_id = getattr(asset.intersection, 'municipality_id', None)
|
|
||||||
if not muni_id and hasattr(asset, 'building') and asset.building:
|
|
||||||
muni_id = getattr(asset.building, 'municipality_id', None)
|
|
||||||
if muni_id is not None:
|
|
||||||
if muni_id not in accessible_muni_ids:
|
|
||||||
return False
|
|
||||||
elif getattr(asset, 'geom', None):
|
|
||||||
from common.models import Municipality
|
|
||||||
if not Municipality.objects.filter(id__in=accessible_muni_ids, geom__intersects=asset.geom).exists():
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return _check_instance_permission(user, asset, 'can_view', _fallback)
|
return _check_instance_permission(user, asset, 'can_view', _fallback)
|
||||||
|
|
@ -588,10 +561,6 @@ def get_allowed_update_fields_for_asset(user, asset):
|
||||||
role_fields = ASSET_EDITABLE_FIELDS_BY_ROLE.get(role.name, [])
|
role_fields = ASSET_EDITABLE_FIELDS_BY_ROLE.get(role.name, [])
|
||||||
allowed_fields.update(role_fields)
|
allowed_fields.update(role_fields)
|
||||||
|
|
||||||
# Superuser fallback if no explicit roles configured
|
|
||||||
if getattr(user, 'is_superuser', False) and not allowed_fields:
|
|
||||||
allowed_fields.update(ASSET_EDITABLE_FIELDS_BY_ROLE.get('admin', []))
|
|
||||||
|
|
||||||
# Check validation permission
|
# Check validation permission
|
||||||
if can_validate_asset(user, asset):
|
if can_validate_asset(user, asset):
|
||||||
allowed_fields.add('validation_status')
|
allowed_fields.add('validation_status')
|
||||||
|
|
@ -916,124 +885,3 @@ def get_user_accessible_municipalities(user):
|
||||||
lang = get_language() or 'fr'
|
lang = get_language() or 'fr'
|
||||||
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||||
return Municipality.objects.filter(id__in=municipality_ids).order_by(order_field)
|
return Municipality.objects.filter(id__in=municipality_ids).order_by(order_field)
|
||||||
|
|
||||||
|
|
||||||
# ─── Management Zones Permissions ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def can_view_management_zones(user, thematic=None):
|
|
||||||
"""
|
|
||||||
Vérifie si l'utilisateur a le droit de visualiser les zones de gestion.
|
|
||||||
Si thematic est fourni (instance Thematic ou code str), vérifie pour cette thématique.
|
|
||||||
"""
|
|
||||||
if not user or not user.is_authenticated:
|
|
||||||
return False
|
|
||||||
if getattr(user, 'is_superuser', False) or user.has_perm('assets.view_managementzone'):
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_config = UserConfig.objects.get(user=user)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if user_config.roles.filter(name__in=['admin', 'operator', 'manager']).exists():
|
|
||||||
return True
|
|
||||||
|
|
||||||
if thematic:
|
|
||||||
thematic_obj = thematic
|
|
||||||
if isinstance(thematic, str):
|
|
||||||
from common.models import Thematic
|
|
||||||
thematic_obj = Thematic.objects.filter(code=thematic).first()
|
|
||||||
if not thematic_obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_thematic = UserThematics.objects.get(user_config=user_config, thematic=thematic_obj)
|
|
||||||
return user_thematic.can_view_assets
|
|
||||||
except UserThematics.DoesNotExist:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return UserThematics.objects.filter(user_config=user_config, can_view_assets=True).exists()
|
|
||||||
|
|
||||||
|
|
||||||
def can_edit_management_zones(user, thematic=None):
|
|
||||||
"""
|
|
||||||
Vérifie si l'utilisateur a le droit de créer ou modifier les zones de gestion.
|
|
||||||
Autorisé si:
|
|
||||||
- Superutilisateur ou permission Django explicite (assets.change_managementzone / assets.add_managementzone)
|
|
||||||
- Rôle 'admin' ou 'manager'
|
|
||||||
- Ou UserThematics.can_edit_management_zones == True (ou fallback can_edit_assets)
|
|
||||||
"""
|
|
||||||
from django.db import models as db_models
|
|
||||||
if not user or not user.is_authenticated:
|
|
||||||
return False
|
|
||||||
if getattr(user, 'is_superuser', False):
|
|
||||||
return True
|
|
||||||
if user.has_perm('assets.change_managementzone') or user.has_perm('assets.add_managementzone'):
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_config = UserConfig.objects.get(user=user)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if user_config.roles.filter(name__in=['admin', 'manager']).exists():
|
|
||||||
return True
|
|
||||||
|
|
||||||
if thematic:
|
|
||||||
thematic_obj = thematic
|
|
||||||
if isinstance(thematic, str):
|
|
||||||
from common.models import Thematic
|
|
||||||
thematic_obj = Thematic.objects.filter(code=thematic).first()
|
|
||||||
if not thematic_obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_thematic = UserThematics.objects.get(user_config=user_config, thematic=thematic_obj)
|
|
||||||
return bool(
|
|
||||||
getattr(user_thematic, 'can_edit_management_zones', False) or
|
|
||||||
user_thematic.can_edit_assets
|
|
||||||
)
|
|
||||||
except UserThematics.DoesNotExist:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return UserThematics.objects.filter(
|
|
||||||
user_config=user_config
|
|
||||||
).filter(
|
|
||||||
db_models.Q(can_edit_management_zones=True) | db_models.Q(can_edit_assets=True)
|
|
||||||
).exists()
|
|
||||||
|
|
||||||
|
|
||||||
def can_delete_management_zone(user, zone):
|
|
||||||
"""Vérifie si l'utilisateur a le droit de supprimer une zone de gestion."""
|
|
||||||
if not user or not user.is_authenticated:
|
|
||||||
return False
|
|
||||||
if getattr(user, 'is_superuser', False) or user.has_perm('assets.delete_managementzone'):
|
|
||||||
return True
|
|
||||||
return can_edit_management_zones(user, thematic=zone.thematic)
|
|
||||||
|
|
||||||
|
|
||||||
def get_editable_thematics_for_management_zones(user):
|
|
||||||
"""
|
|
||||||
Retourne le queryset des thématiques pour lesquelles l'utilisateur peut créer/éditer des zones de gestion.
|
|
||||||
"""
|
|
||||||
from common.models import Thematic
|
|
||||||
from django.db import models as db_models
|
|
||||||
if not user or not user.is_authenticated:
|
|
||||||
return Thematic.objects.none()
|
|
||||||
if getattr(user, 'is_superuser', False) or user.has_perm('assets.change_managementzone'):
|
|
||||||
return Thematic.objects.all()
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_config = UserConfig.objects.get(user=user)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
return Thematic.objects.none()
|
|
||||||
|
|
||||||
if user_config.roles.filter(name__in=['admin', 'manager']).exists():
|
|
||||||
return Thematic.objects.all()
|
|
||||||
|
|
||||||
return Thematic.objects.filter(
|
|
||||||
userthematics__user_config=user_config
|
|
||||||
).filter(
|
|
||||||
db_models.Q(userthematics__can_edit_management_zones=True) |
|
|
||||||
db_models.Q(userthematics__can_edit_assets=True)
|
|
||||||
).distinct()
|
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,6 @@
|
||||||
addIntersectionAssetsLayer(container);
|
addIntersectionAssetsLayer(container);
|
||||||
addConnectionLinesLayer();
|
addConnectionLinesLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure asset layers are stacked above basemaps
|
|
||||||
bringAssetLayersToTop();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,8 +224,6 @@
|
||||||
addLineLayer();
|
addLineLayer();
|
||||||
addPointLayer();
|
addPointLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
bringAssetLayersToTop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPointLayer() {
|
function addPointLayer() {
|
||||||
|
|
@ -656,29 +651,9 @@
|
||||||
'current-asset-icon',
|
'current-asset-icon',
|
||||||
];
|
];
|
||||||
|
|
||||||
function getBasemapBeforeLayer(m) {
|
|
||||||
if (!m || !m.getStyle) return undefined;
|
|
||||||
const styleLayers = (m.getStyle() && m.getStyle().layers) || [];
|
|
||||||
const drawLayer = styleLayers.find(l => l.id && (l.id.startsWith('gl-draw-') || l.id.startsWith('custom-draw-')));
|
|
||||||
if (drawLayer) return drawLayer.id;
|
|
||||||
|
|
||||||
// Return first existing asset layer so basemap is placed below it
|
|
||||||
for (const id of ASSET_LAYER_ORDER) {
|
|
||||||
if (m.getLayer(id)) return id;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bringAssetLayersToTop() {
|
function bringAssetLayersToTop() {
|
||||||
if (!map || typeof map.isStyleLoaded !== 'function' || !map.isStyleLoaded()) return;
|
|
||||||
ASSET_LAYER_ORDER.forEach(id => {
|
ASSET_LAYER_ORDER.forEach(id => {
|
||||||
if (map.getLayer(id)) {
|
if (map.getLayer(id)) map.moveLayer(id);
|
||||||
try {
|
|
||||||
map.moveLayer(id);
|
|
||||||
} catch (e) {
|
|
||||||
// ignore if layer cannot be moved
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -686,25 +661,16 @@
|
||||||
if (typeof LayerSwitcherControl === 'undefined') return;
|
if (typeof LayerSwitcherControl === 'undefined') return;
|
||||||
|
|
||||||
const layerSwitcher = new LayerSwitcherControl({
|
const layerSwitcher = new LayerSwitcherControl({
|
||||||
onBasemapChange: function() {
|
|
||||||
bringAssetLayersToTop();
|
|
||||||
},
|
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
id: 'ortho',
|
id: 'ortho',
|
||||||
name: 'Orthophotos',
|
name: 'Orthophotos',
|
||||||
icon: 'satellite',
|
icon: 'satellite',
|
||||||
group: 'ortho',
|
group: 'ortho',
|
||||||
beforeLayer: getBasemapBeforeLayer,
|
|
||||||
onLayerAdded: () => bringAssetLayersToTop(),
|
|
||||||
onVisibilityChange: (layerId, isVisible) => {
|
|
||||||
if (isVisible) bringAssetLayersToTop();
|
|
||||||
},
|
|
||||||
layer: {
|
layer: {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
paint: { 'raster-opacity': 1 }
|
paint: { 'raster-opacity': 1 }
|
||||||
},
|
},
|
||||||
basemaps: (window.MAP_BACKGROUNDS && window.MAP_BACKGROUNDS.length > 0) ? window.MAP_BACKGROUNDS : undefined,
|
|
||||||
sublayers: [
|
sublayers: [
|
||||||
{
|
{
|
||||||
id: 'urbis',
|
id: 'urbis',
|
||||||
|
|
@ -734,12 +700,6 @@
|
||||||
|
|
||||||
map.addControl(layerSwitcher, 'top-left');
|
map.addControl(layerSwitcher, 'top-left');
|
||||||
|
|
||||||
// Listen for layer switcher events on map to keep asset layers on top
|
|
||||||
map.on('layerswitcher:layerAdded', bringAssetLayersToTop);
|
|
||||||
map.on('layerswitcher:visibilityChange', (e) => {
|
|
||||||
if (e && e.visible) bringAssetLayersToTop();
|
|
||||||
});
|
|
||||||
|
|
||||||
// The LayerSwitcherControl appends sublayer selectors to document.body with
|
// The LayerSwitcherControl appends sublayer selectors to document.body with
|
||||||
// position:fixed and hardcoded coordinates designed for fullscreen maps.
|
// position:fixed and hardcoded coordinates designed for fullscreen maps.
|
||||||
// We move each selector into #asset-map (position:relative) and switch to
|
// We move each selector into #asset-map (position:relative) and switch to
|
||||||
|
|
@ -763,9 +723,8 @@
|
||||||
|
|
||||||
// After any ortho interaction, put asset layers back on top
|
// After any ortho interaction, put asset layers back on top
|
||||||
map.getContainer().addEventListener('click', (e) => {
|
map.getContainer().addEventListener('click', (e) => {
|
||||||
if (e.target.closest('.layer-switcher-btn') || e.target.closest('.layer-switcher-sublayer-btn') || e.target.closest('.layer-switcher-item')) {
|
if (e.target.closest('.layer-switcher-btn') || e.target.closest('.layer-switcher-sublayer-btn')) {
|
||||||
setTimeout(bringAssetLayersToTop, 50);
|
setTimeout(bringAssetLayersToTop, 150);
|
||||||
setTimeout(bringAssetLayersToTop, 200);
|
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
@ -802,10 +761,6 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bringAssetLayersToTop();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.bringAssetLayersToTop = bringAssetLayersToTop;
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -278,10 +278,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof window.bringAssetLayersToTop === 'function') {
|
|
||||||
window.bringAssetLayersToTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove draw control and listeners
|
// Remove draw control and listeners
|
||||||
if (drawInstance && drawControlAdded) {
|
if (drawInstance && drawControlAdded) {
|
||||||
mapInstance.off('draw.create', handleDrawCreate);
|
mapInstance.off('draw.create', handleDrawCreate);
|
||||||
|
|
|
||||||
|
|
@ -261,37 +261,10 @@
|
||||||
glyphCodepoint: 'f30b',
|
glyphCodepoint: 'f30b',
|
||||||
mapImage: 'icon-naturerwiasset',
|
mapImage: 'icon-naturerwiasset',
|
||||||
},
|
},
|
||||||
naturetree_regional: {
|
|
||||||
thematic: 'nature',
|
|
||||||
color: '#1d4ed8',
|
|
||||||
iconUrl: '/static/assets/nature_tree_icon_regional.svg?v=20261009_round',
|
|
||||||
iconSize: 56,
|
|
||||||
iconClass: 'bi-tree',
|
|
||||||
glyphCodepoint: 'f5e1',
|
|
||||||
mapImage: 'icon-nature-regional',
|
|
||||||
},
|
|
||||||
naturetree_remarkable: {
|
|
||||||
thematic: 'nature',
|
|
||||||
color: '#d97706',
|
|
||||||
iconUrl: '/static/assets/nature_tree_icon_remarkable.svg?v=20261009_round',
|
|
||||||
iconSize: 56,
|
|
||||||
iconClass: 'bi-tree',
|
|
||||||
glyphCodepoint: 'f5e1',
|
|
||||||
mapImage: 'icon-nature-remarkable',
|
|
||||||
},
|
|
||||||
naturetree_dead: {
|
|
||||||
thematic: 'nature',
|
|
||||||
color: '#6b7280',
|
|
||||||
iconUrl: '/static/assets/nature_tree_icon_dead.svg?v=20261009_round',
|
|
||||||
iconSize: 56,
|
|
||||||
iconClass: 'bi-tree',
|
|
||||||
glyphCodepoint: 'f5e1',
|
|
||||||
mapImage: 'icon-nature-dead',
|
|
||||||
},
|
|
||||||
naturetree: {
|
naturetree: {
|
||||||
thematic: 'nature',
|
thematic: 'nature',
|
||||||
color: '#2e7d32',
|
color: '#2e7d32',
|
||||||
iconUrl: '/static/assets/nature_tree_icon.svg?v=20261009_round',
|
iconUrl: '/static/assets/nature_tree_icon.svg?v=20260521a',
|
||||||
iconSize: 56,
|
iconSize: 56,
|
||||||
iconClass: 'bi-tree-fill',
|
iconClass: 'bi-tree-fill',
|
||||||
glyphCodepoint: 'f5e1',
|
glyphCodepoint: 'f5e1',
|
||||||
|
|
@ -300,11 +273,11 @@
|
||||||
naturegreensurface: {
|
naturegreensurface: {
|
||||||
thematic: 'nature',
|
thematic: 'nature',
|
||||||
color: '#2e7d32',
|
color: '#2e7d32',
|
||||||
iconUrl: '/static/assets/nature_greensurface_icon.svg?v=20261010a',
|
iconUrl: '/static/assets/nature_tree_icon.svg?v=20260521a',
|
||||||
iconSize: 52,
|
iconSize: 56,
|
||||||
iconClass: 'bi-grid-fill',
|
iconClass: 'bi-tree-fill',
|
||||||
glyphCodepoint: 'f3fb',
|
glyphCodepoint: 'f5e1',
|
||||||
mapImage: 'icon-naturegreensurface',
|
mapImage: 'icon-nature',
|
||||||
},
|
},
|
||||||
natureshrub: {
|
natureshrub: {
|
||||||
thematic: 'nature',
|
thematic: 'nature',
|
||||||
|
|
@ -454,33 +427,6 @@
|
||||||
glyphCodepoint: 'f82b',
|
glyphCodepoint: 'f82b',
|
||||||
mapImage: 'icon-sign',
|
mapImage: 'icon-sign',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ITS
|
|
||||||
itsgeoasset: {
|
|
||||||
thematic: 'its',
|
|
||||||
color: '#0288d1',
|
|
||||||
iconUrl: null,
|
|
||||||
iconClass: 'bi-broadcast-pin',
|
|
||||||
glyphCodepoint: 'f1d5',
|
|
||||||
mapImage: 'icon-its',
|
|
||||||
},
|
|
||||||
itsbollardsystem: {
|
|
||||||
thematic: 'its',
|
|
||||||
color: '#0288d1',
|
|
||||||
iconUrl: null,
|
|
||||||
iconClass: 'bi-border-width',
|
|
||||||
glyphCodepoint: 'f1ca',
|
|
||||||
mapImage: 'icon-itsbollardsystem',
|
|
||||||
},
|
|
||||||
itsbollardplot: {
|
|
||||||
thematic: 'its',
|
|
||||||
color: '#0288d1',
|
|
||||||
iconUrl: '/static/assets/its_bollard_plot_icon.svg?v=20261008',
|
|
||||||
iconSize: 42,
|
|
||||||
iconClass: 'bi-cone-striped',
|
|
||||||
glyphCodepoint: 'f2aa',
|
|
||||||
mapImage: 'icon-itsbollardplot',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── 3. Specific Parking Spot Overrides by spot_type ─────────────────────────
|
// ─── 3. Specific Parking Spot Overrides by spot_type ─────────────────────────
|
||||||
|
|
@ -605,26 +551,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Specific asset_type override
|
// 3. Specific asset_type override
|
||||||
if (assetType === 'naturetree' || (thematic === 'nature' && (assetType === 'naturetree' || !assetType))) {
|
|
||||||
const isDead = item.is_dead === true || item.is_dead === 'true' || item.is_dead === 1 ||
|
|
||||||
item.vitality === 'dead' || item.status === 'dead' || item.status === 'archived';
|
|
||||||
if (isDead && ASSET_TYPE_OVERRIDES.naturetree_dead) {
|
|
||||||
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_dead);
|
|
||||||
}
|
|
||||||
const isRemarkable = item.is_remarkable === true || item.is_remarkable === 'true' || item.is_remarkable === 1;
|
|
||||||
const isRegional = item.management_type === 'regional' ||
|
|
||||||
(typeof item.managing_authority === 'string' && (item.managing_authority.toLowerCase().includes('région') || item.managing_authority.toLowerCase().includes('mobilité'))) ||
|
|
||||||
(typeof item.code === 'string' && item.code.toUpperCase().startsWith('BM-ARB'));
|
|
||||||
if (isRemarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable) {
|
|
||||||
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_remarkable);
|
|
||||||
}
|
|
||||||
if (isRegional && ASSET_TYPE_OVERRIDES.naturetree_regional) {
|
|
||||||
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_regional);
|
|
||||||
}
|
|
||||||
if (ASSET_TYPE_OVERRIDES.naturetree) {
|
|
||||||
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (assetType === 'naturebench') {
|
if (assetType === 'naturebench') {
|
||||||
const isPlanter = (item.asset_sub_type === 'planter') ||
|
const isPlanter = (item.asset_sub_type === 'planter') ||
|
||||||
['Bac à fleurs / Jardinière', 'Bloembak', 'Jardinière', 'Bac à fleurs', 'Bac à plantes'].includes(item.model) ||
|
['Bac à fleurs / Jardinière', 'Bloembak', 'Jardinière', 'Bac à fleurs', 'Bac à plantes'].includes(item.model) ||
|
||||||
|
|
@ -672,17 +598,12 @@
|
||||||
|
|
||||||
const customStyle = options.style ? ` style="${options.style}"` : '';
|
const customStyle = options.style ? ` style="${options.style}"` : '';
|
||||||
|
|
||||||
const classList = ['asset-symbology-icon-img'];
|
|
||||||
if (options.imgClass) classList.push(options.imgClass);
|
|
||||||
const imgClass = classList.join(' ');
|
|
||||||
const defaultImgStyle = 'width: 20px; height: 20px; max-width: 20px; max-height: 20px; object-fit: contain; vertical-align: middle; display: inline-block;';
|
|
||||||
const finalStyle = options.style ? ` style="${defaultImgStyle} ${options.style}"` : ` style="${defaultImgStyle}"`;
|
|
||||||
|
|
||||||
if (symbology.iconUrl) {
|
if (symbology.iconUrl) {
|
||||||
|
const imgClass = options.imgClass || 'asset-symbology-icon-img';
|
||||||
const fallbackUrl = symbology.fallbackUrl || (symbology.iconUrl.includes('.svg') ? symbology.iconUrl.replace(/\.svg(\?.*)?$/, '.png$1') : '');
|
const fallbackUrl = symbology.fallbackUrl || (symbology.iconUrl.includes('.svg') ? symbology.iconUrl.replace(/\.svg(\?.*)?$/, '.png$1') : '');
|
||||||
const fallbackClass = options.iconClass || symbology.iconClass || 'bi-sign-stop-fill';
|
const fallbackClass = options.iconClass || symbology.iconClass || 'bi-sign-stop-fill';
|
||||||
const onerrorAttr = ` onerror="this.onerror=null; ${fallbackUrl ? `if(this.src && this.src.indexOf('.svg') !== -1){ this.src='${fallbackUrl}'; } else {` : ''} this.style.display='none'; if(this.nextElementSibling){ this.nextElementSibling.style.display=''; } ${fallbackUrl ? '}' : ''}"`;
|
const onerrorAttr = ` onerror="this.onerror=null; ${fallbackUrl ? `if(this.src && this.src.indexOf('.svg') !== -1){ this.src='${fallbackUrl}'; } else {` : ''} this.style.display='none'; if(this.nextElementSibling){ this.nextElementSibling.style.display=''; } ${fallbackUrl ? '}' : ''}"`;
|
||||||
return `<img src="${symbology.iconUrl}" alt="" class="${imgClass}"${finalStyle}${onerrorAttr}><i class="bi ${fallbackClass}" style="display:none;${options.style || ''}"></i>`;
|
return `<img src="${symbology.iconUrl}" alt="" class="${imgClass}"${customStyle}${onerrorAttr}><i class="bi ${fallbackClass}" style="display:none;${options.style || ''}"></i>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map';
|
const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map';
|
||||||
|
|
@ -705,10 +626,6 @@
|
||||||
publiclighting: injected.publiclighting || THEMATIC_CONFIG.publiclighting.iconUrl,
|
publiclighting: injected.publiclighting || THEMATIC_CONFIG.publiclighting.iconUrl,
|
||||||
publiclightingelectricalcabinet: injected.publiclightingelectricalcabinet || ASSET_TYPE_OVERRIDES.publiclightingelectricalcabinet.iconUrl,
|
publiclightingelectricalcabinet: injected.publiclightingelectricalcabinet || ASSET_TYPE_OVERRIDES.publiclightingelectricalcabinet.iconUrl,
|
||||||
nature: injected.nature || THEMATIC_CONFIG.nature.iconUrl,
|
nature: injected.nature || THEMATIC_CONFIG.nature.iconUrl,
|
||||||
natureregional: injected.natureregional || (ASSET_TYPE_OVERRIDES.naturetree_regional && ASSET_TYPE_OVERRIDES.naturetree_regional.iconUrl),
|
|
||||||
natureremarkable: injected.natureremarkable || (ASSET_TYPE_OVERRIDES.naturetree_remarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable.iconUrl),
|
|
||||||
naturedead: injected.naturedead || (ASSET_TYPE_OVERRIDES.naturetree_dead && ASSET_TYPE_OVERRIDES.naturetree_dead.iconUrl),
|
|
||||||
naturegreensurface: injected.naturegreensurface || (ASSET_TYPE_OVERRIDES.naturegreensurface && ASSET_TYPE_OVERRIDES.naturegreensurface.iconUrl),
|
|
||||||
structures: injected.structures || THEMATIC_CONFIG.structures.iconUrl,
|
structures: injected.structures || THEMATIC_CONFIG.structures.iconUrl,
|
||||||
roaddrain: injected.roaddrain || ASSET_TYPE_OVERRIDES.roaddrain.iconUrl,
|
roaddrain: injected.roaddrain || ASSET_TYPE_OVERRIDES.roaddrain.iconUrl,
|
||||||
roadconcreteblockset: injected.roadconcreteblockset || ASSET_TYPE_OVERRIDES.roadconcreteblockset.iconUrl,
|
roadconcreteblockset: injected.roadconcreteblockset || ASSET_TYPE_OVERRIDES.roadconcreteblockset.iconUrl,
|
||||||
|
|
@ -716,7 +633,6 @@
|
||||||
natureplanter: injected.natureplanter || (ASSET_TYPE_OVERRIDES.natureplanter && ASSET_TYPE_OVERRIDES.natureplanter.iconUrl),
|
natureplanter: injected.natureplanter || (ASSET_TYPE_OVERRIDES.natureplanter && ASSET_TYPE_OVERRIDES.natureplanter.iconUrl),
|
||||||
artworkfountain: injected.artworkfountain || ASSET_TYPE_OVERRIDES.artworkfountain.iconUrl,
|
artworkfountain: injected.artworkfountain || ASSET_TYPE_OVERRIDES.artworkfountain.iconUrl,
|
||||||
artworkmonument: injected.artworkmonument || ASSET_TYPE_OVERRIDES.artworkmonument.iconUrl,
|
artworkmonument: injected.artworkmonument || ASSET_TYPE_OVERRIDES.artworkmonument.iconUrl,
|
||||||
itsbollardplot: injected.itsbollardplot || (ASSET_TYPE_OVERRIDES.itsbollardplot && ASSET_TYPE_OVERRIDES.itsbollardplot.iconUrl),
|
|
||||||
};
|
};
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
@ -734,16 +650,12 @@
|
||||||
{ name: 'icon-trafficlightradarpole', codepoint: 'f4e0', color: '#e53935' },
|
{ name: 'icon-trafficlightradarpole', codepoint: 'f4e0', color: '#e53935' },
|
||||||
{ name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' },
|
{ name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' },
|
||||||
{ name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' },
|
{ name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' },
|
||||||
{ name: 'icon-naturegreensurface', codepoint: 'f3fb', color: '#2e7d32' },
|
|
||||||
{ name: 'icon-naturebench', codepoint: 'f5e1', color: '#2e7d32' },
|
{ name: 'icon-naturebench', codepoint: 'f5e1', color: '#2e7d32' },
|
||||||
{ name: 'icon-natureplanter', codepoint: 'f3ce', color: '#d97706' },
|
{ name: 'icon-natureplanter', codepoint: 'f3ce', color: '#d97706' },
|
||||||
{ name: 'icon-naturerwiasset', codepoint: 'f30b', color: '#0277bd' },
|
{ name: 'icon-naturerwiasset', codepoint: 'f30b', color: '#0277bd' },
|
||||||
{ name: 'icon-roaddrain', codepoint: 'f617', color: '#1e88e5' },
|
{ name: 'icon-roaddrain', codepoint: 'f617', color: '#1e88e5' },
|
||||||
{ name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' },
|
{ name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' },
|
||||||
{ name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' },
|
{ name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' },
|
||||||
{ name: 'icon-itsgeoasset', codepoint: 'f1d5', color: '#0288d1' },
|
|
||||||
{ name: 'icon-itsbollardsystem', codepoint: 'f1ca', color: '#0288d1' },
|
|
||||||
{ name: 'icon-itsbollardplot', codepoint: 'f2aa', color: '#0288d1' },
|
|
||||||
{ name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' },
|
{ name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' },
|
||||||
{ name: 'icon-cleanglasscontainer', codepoint: 'f78a', color: '#16a34a' },
|
{ name: 'icon-cleanglasscontainer', codepoint: 'f78a', color: '#16a34a' },
|
||||||
{ name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' },
|
{ name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' },
|
||||||
|
|
@ -830,27 +742,6 @@
|
||||||
['==', ['slice', ['get', 'code'], 0, 7], 'CSE-BAC'],
|
['==', ['slice', ['get', 'code'], 0, 7], 'CSE-BAC'],
|
||||||
], 'icon-natureplanter',
|
], 'icon-natureplanter',
|
||||||
['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench',
|
['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench',
|
||||||
['all',
|
|
||||||
['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']],
|
|
||||||
['any',
|
|
||||||
['==', ['get', 'is_dead'], true],
|
|
||||||
['==', ['get', 'is_dead'], 'true'],
|
|
||||||
['==', ['get', 'is_dead'], 1],
|
|
||||||
['==', ['get', 'status'], 'dead'],
|
|
||||||
['==', ['get', 'status'], 'archived'],
|
|
||||||
['==', ['get', 'vitality'], 'dead']
|
|
||||||
]
|
|
||||||
], 'icon-nature-dead',
|
|
||||||
['all',
|
|
||||||
['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']],
|
|
||||||
['any', ['==', ['get', 'is_remarkable'], true], ['==', ['get', 'is_remarkable'], 'true'], ['==', ['get', 'is_remarkable'], 1]]
|
|
||||||
], 'icon-nature-remarkable',
|
|
||||||
['all',
|
|
||||||
['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']],
|
|
||||||
['any', ['==', ['get', 'management_type'], 'regional'], ['==', ['get', 'management_type'], 'Régional']]
|
|
||||||
], 'icon-nature-regional',
|
|
||||||
['==', ['get', 'asset_type'], 'naturetree'], 'icon-nature',
|
|
||||||
['==', ['get', 'asset_type'], 'naturegreensurface'], 'icon-naturegreensurface',
|
|
||||||
['==', ['get', 'asset_type'], 'drain'], 'icon-roaddrain',
|
['==', ['get', 'asset_type'], 'drain'], 'icon-roaddrain',
|
||||||
['==', ['get', 'asset_type'], 'roaddrain'], 'icon-roaddrain',
|
['==', ['get', 'asset_type'], 'roaddrain'], 'icon-roaddrain',
|
||||||
['==', ['get', 'category'], 'Ralentisseur'], 'icon-speedbump',
|
['==', ['get', 'category'], 'Ralentisseur'], 'icon-speedbump',
|
||||||
|
|
@ -858,9 +749,6 @@
|
||||||
['==', ['get', 'asset_type'], 'roadfurniture'], 'icon-speedbump',
|
['==', ['get', 'asset_type'], 'roadfurniture'], 'icon-speedbump',
|
||||||
['==', ['get', 'asset_type'], 'roadconcreteblockset'], 'icon-roadconcreteblockset',
|
['==', ['get', 'asset_type'], 'roadconcreteblockset'], 'icon-roadconcreteblockset',
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'], 'icon-naturerwiasset',
|
['==', ['get', 'asset_type'], 'naturerwiasset'], 'icon-naturerwiasset',
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'], 'icon-itsbollardplot',
|
|
||||||
['==', ['get', 'asset_type'], 'itsbollardsystem'], 'icon-itsbollardsystem',
|
|
||||||
['==', ['get', 'asset_type'], 'itsgeoasset'], 'icon-its',
|
|
||||||
['==', ['get', 'spot_type'], 'scooter_drop_zone'], 'icon-parkingspot-scooter_drop_zone',
|
['==', ['get', 'spot_type'], 'scooter_drop_zone'], 'icon-parkingspot-scooter_drop_zone',
|
||||||
['==', ['get', 'spot_type'], 'pmr'], 'icon-parkingspot-pmr',
|
['==', ['get', 'spot_type'], 'pmr'], 'icon-parkingspot-pmr',
|
||||||
['==', ['get', 'spot_type'], 'ev_charging'], 'icon-parkingspot-ev_charging',
|
['==', ['get', 'spot_type'], 'ev_charging'], 'icon-parkingspot-ev_charging',
|
||||||
|
|
@ -1036,20 +924,14 @@
|
||||||
'#0288d1'
|
'#0288d1'
|
||||||
],
|
],
|
||||||
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
|
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
|
||||||
['==', ['get', 'asset_type'], 'itsbollardsystem'], '#0288d1',
|
|
||||||
['match', ['get', 'asset_type'], 'roadway', '#334155', 'sidewalk', '#64748b', getThematicColorExpr()]
|
['match', ['get', 'asset_type'], 'roadway', '#334155', 'sidewalk', '#64748b', getThematicColorExpr()]
|
||||||
],
|
],
|
||||||
'line-width': [
|
'line-width': [
|
||||||
'case',
|
'case',
|
||||||
['any', ['boolean', ['feature-state', 'highlighted'], false], ['boolean', ['feature-state', 'selected'], false]], 6,
|
['any', ['boolean', ['feature-state', 'highlighted'], false], ['boolean', ['feature-state', 'selected'], false]], 6,
|
||||||
['==', ['get', 'asset_type'], 'itsbollardsystem'], 5,
|
|
||||||
3.5
|
3.5
|
||||||
],
|
],
|
||||||
'line-opacity': [
|
'line-opacity': 0.88,
|
||||||
'case',
|
|
||||||
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0.95,
|
|
||||||
0.88
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
minzoom: 8,
|
minzoom: 8,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -79,21 +79,6 @@ const draw = new MapboxDraw({
|
||||||
map.on('load', () => {
|
map.on('load', () => {
|
||||||
map.addControl(draw);
|
map.addControl(draw);
|
||||||
|
|
||||||
// Contrôle sélecteur de couches (LayerSwitcherControl)
|
|
||||||
if (typeof LayerSwitcherControl !== 'undefined') {
|
|
||||||
const layerSwitcher = new LayerSwitcherControl({
|
|
||||||
layers: [{
|
|
||||||
id: 'basemap',
|
|
||||||
name: (typeof gettext === 'function') ? gettext('Fonds de plan & Orthophotos') : 'Fonds de plan & Orthophotos',
|
|
||||||
icon: 'satellite',
|
|
||||||
beforeLayer: 'locations',
|
|
||||||
basemaps: window.MAP_BACKGROUNDS || [],
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
map.addControl(layerSwitcher, 'top-right');
|
|
||||||
window.layerSwitcher = layerSwitcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
map.boxZoom.enable();
|
map.boxZoom.enable();
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
|
|
|
||||||
44
loko/assets/static/assets/green_detail.js
Normal file
44
loko/assets/static/assets/green_detail.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
style: window.MAP_STYLE_LIGHT,
|
||||||
|
center: [feature_lon, feature_lat],
|
||||||
|
zoom: 17,
|
||||||
|
pitch: 45,
|
||||||
|
bearing: -17.6,
|
||||||
|
container: 'map',
|
||||||
|
antialias: true
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on('load', () => {
|
||||||
|
|
||||||
|
const featureData = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: JSON.parse(feature_geojson),
|
||||||
|
properties: {
|
||||||
|
name: '...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the circle as a GeoJSON source
|
||||||
|
map.addSource('green', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: featureData
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
'id': 'green',
|
||||||
|
'type': 'fill',
|
||||||
|
'source': 'green',
|
||||||
|
'paint': {
|
||||||
|
'fill-color': '#0094ff',
|
||||||
|
'fill-opacity': 0.8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -89,11 +89,9 @@
|
||||||
|
|
||||||
if (typeof AddressSearchControl !== 'undefined') {
|
if (typeof AddressSearchControl !== 'undefined') {
|
||||||
const autocompleteUrl = window.AUTOCOMPLETE_STREET_URL || '/common/autocomplete-street/';
|
const autocompleteUrl = window.AUTOCOMPLETE_STREET_URL || '/common/autocomplete-street/';
|
||||||
const i18n = window.INSPECTION_MAP_I18N || {};
|
|
||||||
const placeholder = i18n.searchAddress || (typeof gettext !== 'undefined' ? gettext('Rechercher une adresse…') : 'Rechercher une adresse…');
|
|
||||||
const searchCtrl = new AddressSearchControl({
|
const searchCtrl = new AddressSearchControl({
|
||||||
autocompleteUrl: autocompleteUrl,
|
autocompleteUrl: autocompleteUrl,
|
||||||
placeholder: placeholder
|
placeholder: 'Rechercher une adresse...'
|
||||||
});
|
});
|
||||||
map.addControl(searchCtrl, 'top-left');
|
map.addControl(searchCtrl, 'top-left');
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +288,7 @@
|
||||||
'match', ['coalesce', ['get', 'structure_type'], ''],
|
'match', ['coalesce', ['get', 'structure_type'], ''],
|
||||||
['massif drainant', 'tranchee drainante', 'drain', 'collecteur en beton poreux', 'reseau egout separatif', 'oed'], '#8d6e63',
|
['massif drainant', 'tranchee drainante', 'drain', 'collecteur en beton poreux', 'reseau egout separatif', 'oed'], '#8d6e63',
|
||||||
['noue', 'jardin de pluie', 'bande filtrante', 'evc'], '#009688',
|
['noue', 'jardin de pluie', 'bande filtrante', 'evc'], '#009688',
|
||||||
['filet eau', 'caniveau infiltrant', 'gargouille de trottoir', 'avaloir de rail', 'trop plein', 'ta'], '#455a64',
|
['filet eau', 'caniveau infiltrant', 'gargouille de trottoir', 'avaloir de rail', 'trop plein', 'ta'], '#0277bd',
|
||||||
'#0288d1'
|
'#0288d1'
|
||||||
],
|
],
|
||||||
'line-width': 2.5
|
'line-width': 2.5
|
||||||
|
|
@ -343,13 +341,11 @@
|
||||||
['==', ['coalesce', ['get', 'status'], ''], 'inactive'], '#343a40',
|
['==', ['coalesce', ['get', 'status'], ''], 'inactive'], '#343a40',
|
||||||
'#198754'
|
'#198754'
|
||||||
],
|
],
|
||||||
['match', ['coalesce', ['get', 'model_name'], ''], ['roaddrain', 'drain'], true, false], '#37474f',
|
|
||||||
[
|
[
|
||||||
'match', ['coalesce', ['get', 'structure_type'], ''],
|
'match', ['coalesce', ['get', 'structure_type'], ''],
|
||||||
['massif drainant', 'tranchee drainante', 'oed'], '#8d6e63',
|
['massif drainant', 'tranchee drainante', 'oed'], '#8d6e63',
|
||||||
['jardin de pluie', 'noue', 'evc'], '#009688',
|
['jardin de pluie', 'noue', 'evc'], '#009688',
|
||||||
['bassin orage', 'citerne', 'ta'], '#0277bd',
|
['bassin orage', 'citerne', 'ta'], '#0277bd',
|
||||||
['avaloir', 'avaloir de rail', 'bouche injection', 'filet eau', 'caniveau infiltrant', 'gargouille de trottoir', 'puits infiltration', 'drain', 'regard', 'vanne regulation', 'vanne isolation'], '#37474f',
|
|
||||||
'#0288d1'
|
'#0288d1'
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
@ -733,99 +729,50 @@
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
let gpsWatchId = null;
|
|
||||||
let isFirstGpsFix = true;
|
|
||||||
|
|
||||||
function setupGeolocation() {
|
function setupGeolocation() {
|
||||||
const gpsBtn = document.getElementById('btn-gps-locate');
|
const gpsBtn = document.getElementById('btn-gps-locate');
|
||||||
if (gpsBtn) {
|
if (gpsBtn) {
|
||||||
gpsBtn.addEventListener('click', function() {
|
gpsBtn.addEventListener('click', locateUser);
|
||||||
if (window.currentGpsPosition && map) {
|
|
||||||
map.flyTo({
|
|
||||||
center: [window.currentGpsPosition.lng, window.currentGpsPosition.lat],
|
|
||||||
zoom: 18,
|
|
||||||
duration: 800
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
startGpsTracking(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasPreselect = (window.PRESELECT_LON !== null && window.PRESELECT_LAT !== null && typeof window.PRESELECT_LON !== 'undefined' && typeof window.PRESELECT_LAT !== 'undefined');
|
const hasPreselect = (window.PRESELECT_LON !== null && window.PRESELECT_LAT !== null && typeof window.PRESELECT_LON !== 'undefined' && typeof window.PRESELECT_LAT !== 'undefined');
|
||||||
startGpsTracking(!hasPreselect);
|
if (!hasPreselect) {
|
||||||
|
locateUser();
|
||||||
window.addEventListener('beforeunload', stopGpsTracking);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopGpsTracking() {
|
|
||||||
if (gpsWatchId !== null && navigator.geolocation) {
|
|
||||||
navigator.geolocation.clearWatch(gpsWatchId);
|
|
||||||
gpsWatchId = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function locateUser() {
|
function locateUser() {
|
||||||
startGpsTracking(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function startGpsTracking(shouldFlyTo = false) {
|
|
||||||
if (!navigator.geolocation) return;
|
if (!navigator.geolocation) return;
|
||||||
|
|
||||||
const i18n = window.INSPECTION_MAP_I18N || {};
|
|
||||||
const statusText = document.getElementById('gps-status-text');
|
const statusText = document.getElementById('gps-status-text');
|
||||||
if (statusText) statusText.textContent = i18n.geolocation || (typeof gettext !== 'undefined' ? gettext('Géolocalisation...') : 'Géolocalisation...');
|
if (statusText) statusText.textContent = 'Géolocalisation...';
|
||||||
|
|
||||||
if (gpsWatchId !== null) {
|
navigator.geolocation.getCurrentPosition(
|
||||||
navigator.geolocation.clearWatch(gpsWatchId);
|
|
||||||
gpsWatchId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
gpsWatchId = navigator.geolocation.watchPosition(
|
|
||||||
function(position) {
|
function(position) {
|
||||||
const lng = position.coords.longitude;
|
const lng = position.coords.longitude;
|
||||||
const lat = position.coords.latitude;
|
const lat = position.coords.latitude;
|
||||||
const accuracy = position.coords.accuracy;
|
|
||||||
|
|
||||||
window.currentGpsPosition = { lng, lat, accuracy, timestamp: Date.now() };
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.dispatchEvent(new CustomEvent('loko:gps-update', {
|
|
||||||
detail: window.currentGpsPosition
|
|
||||||
}));
|
|
||||||
} catch(e) {}
|
|
||||||
|
|
||||||
if (typeof maplibregl !== 'undefined' && maplibregl.Marker && map) {
|
if (typeof maplibregl !== 'undefined' && maplibregl.Marker && map) {
|
||||||
if (userMarker) {
|
if (userMarker) {
|
||||||
userMarker.setLngLat([lng, lat]);
|
userMarker.setLngLat([lng, lat]);
|
||||||
} else {
|
} else {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'user-gps-dot-container';
|
el.className = 'user-gps-dot';
|
||||||
el.innerHTML = '<div class="user-gps-dot-pulse"></div><div class="user-gps-dot"></div>';
|
|
||||||
|
|
||||||
userMarker = new maplibregl.Marker({ element: el, anchor: 'center' })
|
userMarker = new maplibregl.Marker({ element: el })
|
||||||
.setLngLat([lng, lat])
|
.setLngLat([lng, lat])
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldFlyTo || isFirstGpsFix) {
|
map.flyTo({ center: [lng, lat], zoom: 17 });
|
||||||
map.flyTo({ center: [lng, lat], zoom: 18, duration: 1000 });
|
|
||||||
isFirstGpsFix = false;
|
|
||||||
shouldFlyTo = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusText) {
|
|
||||||
statusText.textContent = i18n.positionUpdated || (typeof gettext !== 'undefined' ? gettext('Position actualisée') : 'Position actualisée');
|
|
||||||
}
|
}
|
||||||
|
if (statusText) statusText.textContent = 'Position actualisée';
|
||||||
},
|
},
|
||||||
function(err) {
|
function(err) {
|
||||||
console.warn('GPS watchPosition error:', err.message);
|
console.warn('GPS error:', err.message);
|
||||||
if (statusText) {
|
if (statusText) statusText.textContent = 'GPS indisponible';
|
||||||
statusText.textContent = i18n.gpsUnavailable || (typeof gettext !== 'undefined' ? gettext('GPS indisponible') : 'GPS indisponible');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 2000 }
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 5000 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -984,17 +931,13 @@
|
||||||
const STORAGE_HISTORY_PERIOD_KEY = 'loko_insp_history_period';
|
const STORAGE_HISTORY_PERIOD_KEY = 'loko_insp_history_period';
|
||||||
|
|
||||||
function getHistoryPeriodLabels() {
|
function getHistoryPeriodLabels() {
|
||||||
const i18n = window.INSPECTION_MAP_I18N || {};
|
|
||||||
if (i18n.periodLabels) {
|
|
||||||
return i18n.periodLabels;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
'disabled': (typeof gettext !== 'undefined' ? gettext('Inspections (Désactivé)') : 'Inspections (Désactivé)'),
|
'disabled': 'Inspections (Désactivé)',
|
||||||
'1m': (typeof gettext !== 'undefined' ? gettext('Inspections (1 mois)') : 'Inspections (1 mois)'),
|
'1m': 'Inspections (1 mois)',
|
||||||
'3m': (typeof gettext !== 'undefined' ? gettext('Inspections (3 mois)') : 'Inspections (3 mois)'),
|
'3m': 'Inspections (3 mois)',
|
||||||
'6m': (typeof gettext !== 'undefined' ? gettext('Inspections (6 mois)') : 'Inspections (6 mois)'),
|
'6m': 'Inspections (6 mois)',
|
||||||
'1y': (typeof gettext !== 'undefined' ? gettext('Inspections (1 an)') : 'Inspections (1 an)'),
|
'1y': 'Inspections (1 an)',
|
||||||
'all': (typeof gettext !== 'undefined' ? gettext('Inspections (Tout)') : 'Inspections (Tout)')
|
'all': 'Inspections (Tout)'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1016,7 +959,7 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (labelEl) labelEl.textContent = labels[period] || labels['3m'] || (typeof gettext !== 'undefined' ? gettext('Inspections (3 mois)') : 'Inspections (3 mois)');
|
if (labelEl) labelEl.textContent = labels[period] || 'Inspections (3 mois)';
|
||||||
if (iconEl) {
|
if (iconEl) {
|
||||||
if (period === 'disabled') {
|
if (period === 'disabled') {
|
||||||
iconEl.className = 'bi bi-eye-slash text-muted me-1';
|
iconEl.className = 'bi bi-eye-slash text-muted me-1';
|
||||||
|
|
@ -1318,7 +1261,7 @@
|
||||||
document.getElementById('map-insp-summary').textContent = insp.details_summary || 'Aucun constat spécifique.';
|
document.getElementById('map-insp-summary').textContent = insp.details_summary || 'Aucun constat spécifique.';
|
||||||
const notesEl = document.getElementById('map-insp-notes');
|
const notesEl = document.getElementById('map-insp-notes');
|
||||||
if (notesEl) {
|
if (notesEl) {
|
||||||
if (insp.notes && !insp.is_giep) {
|
if (insp.notes) {
|
||||||
notesEl.textContent = insp.notes;
|
notesEl.textContent = insp.notes;
|
||||||
notesEl.classList.remove('d-none');
|
notesEl.classList.remove('d-none');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1472,23 +1415,6 @@
|
||||||
window.currentGiepLocationId = props.location_id || (props.is_location ? objId : null) || objId;
|
window.currentGiepLocationId = props.location_id || (props.is_location ? objId : null) || objId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGiep = (modelName === 'naturerwiz' || isZoneRwiz);
|
|
||||||
const quickNotesSection = document.getElementById('quickInspNotesSection');
|
|
||||||
const quickResultSection = document.getElementById('quickInspResultSection');
|
|
||||||
const quickPhotosSection = document.getElementById('quickInspPhotosSection');
|
|
||||||
if (quickNotesSection) {
|
|
||||||
if (isGiep) quickNotesSection.classList.add('d-none');
|
|
||||||
else quickNotesSection.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
if (quickResultSection) {
|
|
||||||
if (isGiep) quickResultSection.classList.add('d-none');
|
|
||||||
else quickResultSection.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
if (quickPhotosSection) {
|
|
||||||
if (isGiep) quickPhotosSection.classList.add('d-none');
|
|
||||||
else quickPhotosSection.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('insp_target_type').textContent = props.element_type || props.asset_type_display || props.model_name || 'Asset';
|
document.getElementById('insp_target_type').textContent = props.element_type || props.asset_type_display || props.model_name || 'Asset';
|
||||||
document.getElementById('insp_target_code').textContent = props.code || props.target_display || (objId ? ('#' + objId) : '-');
|
document.getElementById('insp_target_code').textContent = props.code || props.target_display || (objId ? ('#' + objId) : '-');
|
||||||
document.getElementById('insp_target_name').textContent = props.name || props.name_fr || props.target_display || '-';
|
document.getElementById('insp_target_name').textContent = props.name || props.name_fr || props.target_display || '-';
|
||||||
|
|
@ -2054,9 +1980,9 @@
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
<span class="badge bg-primary rounded-pill small">#${order} (${labelFr || (window.gettext ? window.gettext('Panneau') : 'Panneau')})</span>
|
<span class="badge bg-primary rounded-pill small">#${order} (${labelFr || 'Panneau'})</span>
|
||||||
${confidence ? `<span class="badge bg-success-subtle text-success border border-success-subtle extra-small">IA: ${Math.round(confidence * 100)}%</span>` : ''}
|
${confidence ? `<span class="badge bg-success-subtle text-success border border-success-subtle extra-small">IA: ${Math.round(confidence * 100)}%</span>` : ''}
|
||||||
<button type="button" class="btn btn-outline-danger btn-xs py-0 px-1.5 btn-remove-panel-row" style="font-size: 0.75rem;" title="${window.gettext ? window.gettext('Supprimer ce panneau') : 'Supprimer ce panneau'}">
|
<button type="button" class="btn btn-outline-danger btn-xs py-0 px-1.5 btn-remove-panel-row" style="font-size: 0.75rem;" title="Supprimer ce panneau">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2068,23 +1994,23 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="input-group input-group-sm mb-1">
|
<div class="input-group input-group-sm mb-1">
|
||||||
<span class="input-group-text fw-bold">${window.gettext ? window.gettext('Code') : 'Code'}</span>
|
<span class="input-group-text fw-bold">Code</span>
|
||||||
<input type="text" class="form-control form-control-sm text-uppercase panel-row-code" value="${upperCode}" placeholder="Ex: C1, D7, B1, M2...">
|
<input type="text" class="form-control form-control-sm text-uppercase panel-row-code" value="${upperCode}" placeholder="Ex: C1, D7, B1, M2...">
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control form-control-sm panel-row-text" value="${text || ''}" placeholder="${window.gettext ? window.gettext('Mention (ex: Sauf riverains)') : 'Mention (ex: Sauf riverains)'}">
|
<input type="text" class="form-control form-control-sm panel-row-text" value="${text || ''}" placeholder="Mention (ex: Sauf riverains)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 pt-1 border-top d-flex align-items-center justify-content-between">
|
<div class="mt-2 pt-1 border-top d-flex align-items-center justify-content-between">
|
||||||
<small class="text-muted extra-small">${window.gettext ? window.gettext('Propreté :') : 'Propreté :'}</small>
|
<small class="text-muted extra-small">Propreté :</small>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
<input type="radio" class="btn-check panel-row-clean" name="row_clean_${order}_${Date.now()}" id="rc_clean_${order}_${Date.now()}" value="clean" ${cleanliness === 'clean' ? 'checked' : ''}>
|
<input type="radio" class="btn-check panel-row-clean" name="row_clean_${order}_${Date.now()}" id="rc_clean_${order}_${Date.now()}" value="clean" ${cleanliness === 'clean' ? 'checked' : ''}>
|
||||||
<label class="btn btn-outline-success btn-xs py-0 px-2" for="rc_clean_${order}_${Date.now()}">${window.gettext ? window.gettext('Propre') : 'Propre'}</label>
|
<label class="btn btn-outline-success btn-xs py-0 px-2" for="rc_clean_${order}_${Date.now()}">Propre</label>
|
||||||
<input type="radio" class="btn-check panel-row-clean" name="row_clean_${order}_${Date.now()}" id="rc_dirty_${order}_${Date.now()}" value="dirty" ${cleanliness === 'dirty' ? 'checked' : ''}>
|
<input type="radio" class="btn-check panel-row-clean" name="row_clean_${order}_${Date.now()}" id="rc_dirty_${order}_${Date.now()}" value="dirty" ${cleanliness === 'dirty' ? 'checked' : ''}>
|
||||||
<label class="btn btn-outline-warning btn-xs py-0 px-2" for="rc_dirty_${order}_${Date.now()}">${window.gettext ? window.gettext('Sale') : 'Sale'}</label>
|
<label class="btn btn-outline-warning btn-xs py-0 px-2" for="rc_dirty_${order}_${Date.now()}">Sale</label>
|
||||||
<input type="radio" class="btn-check panel-row-clean" name="row_clean_${order}_${Date.now()}" id="rc_veg_${order}_${Date.now()}" value="vegetation" ${cleanliness === 'vegetation' ? 'checked' : ''}>
|
<input type="radio" class="btn-check panel-row-clean" name="row_clean_${order}_${Date.now()}" id="rc_veg_${order}_${Date.now()}" value="vegetation" ${cleanliness === 'vegetation' ? 'checked' : ''}>
|
||||||
<label class="btn btn-outline-warning btn-xs py-0 px-2" for="rc_veg_${order}_${Date.now()}">${window.gettext ? window.gettext('Végétation') : 'Végétation'}</label>
|
<label class="btn btn-outline-warning btn-xs py-0 px-2" for="rc_veg_${order}_${Date.now()}">Végétation</label>
|
||||||
<input type="radio" class="btn-check panel-row-clean" name="row_clean_${order}_${Date.now()}" id="rc_vand_${order}_${Date.now()}" value="vandalized" ${cleanliness === 'vandalized' ? 'checked' : ''}>
|
<input type="radio" class="btn-check panel-row-clean" name="row_clean_${order}_${Date.now()}" id="rc_vand_${order}_${Date.now()}" value="vandalized" ${cleanliness === 'vandalized' ? 'checked' : ''}>
|
||||||
<label class="btn btn-outline-danger btn-xs py-0 px-2" for="rc_vand_${order}_${Date.now()}">${window.gettext ? window.gettext('Dégradé') : 'Dégradé'}</label>
|
<label class="btn btn-outline-danger btn-xs py-0 px-2" for="rc_vand_${order}_${Date.now()}">Dégradé</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -2117,13 +2043,13 @@
|
||||||
const lon = document.getElementById('new_sign_lon')?.value;
|
const lon = document.getElementById('new_sign_lon')?.value;
|
||||||
|
|
||||||
if (!lat || !lon) {
|
if (!lat || !lon) {
|
||||||
alert(window.gettext ? window.gettext('Coordonnées GPS manquantes.') : 'Coordonnées GPS manquantes.');
|
alert('Coordonnées GPS manquantes.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = document.querySelectorAll('.new-sign-panel-row');
|
const rows = document.querySelectorAll('.new-sign-panel-row');
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
alert(window.gettext ? window.gettext('Veuillez ajouter au moins un panneau.') : 'Veuillez ajouter au moins un panneau.');
|
alert('Veuillez ajouter au moins un panneau.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2148,8 +2074,7 @@
|
||||||
newSignPhotos.forEach(p => formData.append('photos', p));
|
newSignPhotos.forEach(p => formData.append('photos', p));
|
||||||
|
|
||||||
btnSubmit.disabled = true;
|
btnSubmit.disabled = true;
|
||||||
const creatingMsg = window.gettext ? window.gettext('Création en cours...') : 'Création en cours...';
|
btnSubmit.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Création en cours...';
|
||||||
btnSubmit.innerHTML = `<span class="spinner-border spinner-border-sm me-1"></span> ${creatingMsg}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const csrfToken = getCsrfToken();
|
const csrfToken = getCsrfToken();
|
||||||
|
|
@ -2170,7 +2095,7 @@
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
modal.hide();
|
modal.hide();
|
||||||
alert(data.message || (window.gettext ? window.gettext('Panneaux créés avec succès !') : 'Panneaux créés avec succès !'));
|
alert(data.message || 'Panneaux créés avec succès !');
|
||||||
|
|
||||||
// Basculer automatiquement sur la thématique Signalisation
|
// Basculer automatiquement sur la thématique Signalisation
|
||||||
localStorage.setItem(STORAGE_THEMATIC_KEY, 'sign');
|
localStorage.setItem(STORAGE_THEMATIC_KEY, 'sign');
|
||||||
|
|
@ -2188,17 +2113,13 @@
|
||||||
loadAssets();
|
loadAssets();
|
||||||
loadInspectionsHistory();
|
loadInspectionsHistory();
|
||||||
} else {
|
} else {
|
||||||
const errPrefix = window.gettext ? window.gettext('Erreur : ') : 'Erreur : ';
|
alert('Erreur : ' + (data.message || 'Impossible de créer les panneaux.'));
|
||||||
const defaultErrMsg = window.gettext ? window.gettext('Impossible de créer les panneaux.') : 'Impossible de créer les panneaux.';
|
|
||||||
alert(errPrefix + (data.message || defaultErrMsg));
|
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
const errCreationMsg = window.gettext ? window.gettext('Erreur lors de la création : ') : 'Erreur lors de la création : ';
|
alert('Erreur lors de la création : ' + err.message);
|
||||||
alert(errCreationMsg + err.message);
|
|
||||||
} finally {
|
} finally {
|
||||||
btnSubmit.disabled = false;
|
btnSubmit.disabled = false;
|
||||||
const createAndSaveMsg = window.gettext ? window.gettext('Créer et Enregistrer') : 'Créer et Enregistrer';
|
btnSubmit.innerHTML = '<i class="bi bi-check2-circle fs-6"></i> <span>Créer et Enregistrer</span>';
|
||||||
btnSubmit.innerHTML = `<i class="bi bi-check2-circle fs-6"></i> <span>${createAndSaveMsg}</span>`;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2255,22 +2176,19 @@
|
||||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||||
if (modal) modal.hide();
|
if (modal) modal.hide();
|
||||||
|
|
||||||
let msg = data.message || (window.gettext ? window.gettext('Inspection enregistrée !') : 'Inspection enregistrée !');
|
let msg = data.message || 'Inspection enregistrée !';
|
||||||
if (data.created_intervention_code) {
|
if (data.created_intervention_code) {
|
||||||
const intervLabel = window.gettext ? window.gettext('Intervention') : 'Intervention';
|
msg += ' Intervention ' + data.created_intervention_code + ' créée.';
|
||||||
const createdLabel = window.gettext ? window.gettext('créée.') : 'créée.';
|
|
||||||
msg += ` ${intervLabel} ${data.created_intervention_code} ${createdLabel}`;
|
|
||||||
}
|
}
|
||||||
alert(msg);
|
alert(msg);
|
||||||
loadAssets();
|
loadAssets();
|
||||||
} else {
|
} else {
|
||||||
const defaultValError = window.gettext ? window.gettext('Erreur lors de la validation.') : 'Erreur lors de la validation.';
|
alertDiv.textContent = data.error || 'Erreur lors de la validation.';
|
||||||
alertDiv.textContent = data.error || defaultValError;
|
|
||||||
alertDiv.classList.remove('d-none');
|
alertDiv.classList.remove('d-none');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
alertDiv.textContent = window.gettext ? window.gettext('Erreur réseau.') : 'Erreur réseau.';
|
alertDiv.textContent = 'Erreur réseau.';
|
||||||
alertDiv.classList.remove('d-none');
|
alertDiv.classList.remove('d-none');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
|
||||||
<ellipse cx="32" cy="54" rx="22" ry="5" fill="#1e293b" opacity="0.3"/>
|
|
||||||
<ellipse cx="32" cy="52" rx="18" ry="4" fill="#334155"/>
|
|
||||||
<ellipse cx="32" cy="51" rx="16" ry="3.5" fill="#64748b"/>
|
|
||||||
<ellipse cx="32" cy="50.5" rx="13" ry="2.8" fill="#1e293b"/>
|
|
||||||
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bollardGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" stop-color="#0284c7"/>
|
|
||||||
<stop offset="25%" stop-color="#38bdf8"/>
|
|
||||||
<stop offset="60%" stop-color="#0284c7"/>
|
|
||||||
<stop offset="100%" stop-color="#0369a1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="capGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stop-color="#7dd3fc"/>
|
|
||||||
<stop offset="100%" stop-color="#0284c7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="bandGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" stop-color="#f59e0b"/>
|
|
||||||
<stop offset="35%" stop-color="#fef08a"/>
|
|
||||||
<stop offset="70%" stop-color="#f59e0b"/>
|
|
||||||
<stop offset="100%" stop-color="#d97706"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<path d="M 21,24 L 21,50 C 21,53 43,53 43,50 L 43,24 Z" fill="url(#bollardGrad)"/>
|
|
||||||
<path d="M 21,21 L 21,27 C 21,29.5 43,29.5 43,27 L 43,21 C 43,23.5 21,23.5 21,21 Z" fill="url(#bandGrad)"/>
|
|
||||||
<path d="M 21,15 L 21,21 C 21,23.5 43,23.5 43,21 L 43,15 Z" fill="url(#bollardGrad)"/>
|
|
||||||
|
|
||||||
<ellipse cx="32" cy="15" rx="11" ry="3.5" fill="url(#capGrad)" stroke="#bae6fd" stroke-width="0.8"/>
|
|
||||||
<ellipse cx="32" cy="14.2" rx="7" ry="2" fill="#ffffff" opacity="0.6"/>
|
|
||||||
|
|
||||||
<circle cx="26" cy="24.5" r="1" fill="#ffffff"/>
|
|
||||||
<circle cx="32" cy="25.2" r="1.1" fill="#ffffff"/>
|
|
||||||
<circle cx="38" cy="24.5" r="1" fill="#ffffff"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,405 +0,0 @@
|
||||||
/**
|
|
||||||
* Management Zones Map Overlay & Layer Switcher Integration
|
|
||||||
*
|
|
||||||
* Permet d'afficher en surimpression les zones de gestion (gestion écologique,
|
|
||||||
* ondes vertes, secteurs prioritaires, etc.) sur la carte MapLibre
|
|
||||||
* et ajoute une section dédiée "Zones de gestion" dans le LayerSwitcherControl.
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function (window) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calcule la bounding box [[minX, minY], [maxX, maxY]] d'une géométrie GeoJSON.
|
|
||||||
*/
|
|
||||||
function getGeometryBBox(geometry) {
|
|
||||||
if (!geometry || !geometry.coordinates) return null;
|
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
||||||
|
|
||||||
function traverse(coords) {
|
|
||||||
if (typeof coords[0] === 'number') {
|
|
||||||
const [x, y] = coords;
|
|
||||||
if (x < minX) minX = x;
|
|
||||||
if (x > maxX) maxX = x;
|
|
||||||
if (y < minY) minY = y;
|
|
||||||
if (y > maxY) maxY = y;
|
|
||||||
} else if (Array.isArray(coords)) {
|
|
||||||
for (let i = 0; i < coords.length; i++) {
|
|
||||||
traverse(coords[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse(geometry.coordinates);
|
|
||||||
if (minX === Infinity || minY === Infinity) return null;
|
|
||||||
return [[minX, minY], [maxX, maxY]];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise l'overlay des zones de gestion sur la carte MapLibre.
|
|
||||||
*/
|
|
||||||
async function initManagementZonesMapOverlay(options = {}) {
|
|
||||||
const map = options.map;
|
|
||||||
if (!map) {
|
|
||||||
console.warn('[ManagementZonesMapOverlay] Map instance missing.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.loaded()) {
|
|
||||||
map.once('load', () => initManagementZonesMapOverlay(options));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thematic = options.thematic || 'nature';
|
|
||||||
const geojsonUrl = options.geojsonUrl || '/assets/api/management-zones/geojson/';
|
|
||||||
const layerSwitcher = options.layerSwitcher || window.layerSwitcher;
|
|
||||||
const beforeLayerId = options.beforeLayerId || null;
|
|
||||||
|
|
||||||
const _t = (key, fallback) => (typeof gettext === 'function' ? gettext(key) : (fallback || key));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = new URL(geojsonUrl, window.location.origin);
|
|
||||||
url.searchParams.set('thematic', thematic);
|
|
||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn('[ManagementZonesMapOverlay] Failed to fetch geojson:', response.status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const geojson = await response.json();
|
|
||||||
if (!geojson || geojson.type !== 'FeatureCollection') {
|
|
||||||
console.warn('[ManagementZonesMapOverlay] Invalid GeoJSON received.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const features = geojson.features || [];
|
|
||||||
|
|
||||||
// 1. Ajouter la source et les couches à MapLibre
|
|
||||||
const sourceId = 'management-zones-source';
|
|
||||||
const fillLayerId = 'management-zones-fill';
|
|
||||||
const lineLayerId = 'management-zones-line';
|
|
||||||
|
|
||||||
if (map.getSource(sourceId)) {
|
|
||||||
map.getSource(sourceId).setData(geojson);
|
|
||||||
} else {
|
|
||||||
map.addSource(sourceId, {
|
|
||||||
type: 'geojson',
|
|
||||||
data: geojson
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couche de remplissage translucide
|
|
||||||
if (!map.getLayer(fillLayerId)) {
|
|
||||||
const fillLayerDef = {
|
|
||||||
id: fillLayerId,
|
|
||||||
type: 'fill',
|
|
||||||
source: sourceId,
|
|
||||||
paint: {
|
|
||||||
'fill-color': ['coalesce', ['get', 'color'], '#198754'],
|
|
||||||
'fill-opacity': 0.20
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (beforeLayerId && map.getLayer(beforeLayerId)) {
|
|
||||||
map.addLayer(fillLayerDef, beforeLayerId);
|
|
||||||
} else {
|
|
||||||
map.addLayer(fillLayerDef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couche de contour avec tirets pour marquer une zone de gestion
|
|
||||||
if (!map.getLayer(lineLayerId)) {
|
|
||||||
const lineLayerDef = {
|
|
||||||
id: lineLayerId,
|
|
||||||
type: 'line',
|
|
||||||
source: sourceId,
|
|
||||||
paint: {
|
|
||||||
'line-color': ['coalesce', ['get', 'color'], '#198754'],
|
|
||||||
'line-width': 2.5,
|
|
||||||
'line-opacity': 0.85,
|
|
||||||
'line-dasharray': [3, 1.5]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (beforeLayerId && map.getLayer(beforeLayerId)) {
|
|
||||||
map.addLayer(lineLayerDef, beforeLayerId);
|
|
||||||
} else {
|
|
||||||
map.addLayer(lineLayerDef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Gestion des popups et du curseur sur clic
|
|
||||||
const zonePopup = new maplibregl.Popup({
|
|
||||||
closeButton: true,
|
|
||||||
closeOnClick: true,
|
|
||||||
maxWidth: '340px'
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on('click', fillLayerId, (e) => {
|
|
||||||
const assetLayerIds = (window.assets_layers_defs || []).map(l => l.id).filter(id => map.getLayer(id));
|
|
||||||
if (assetLayerIds.length > 0) {
|
|
||||||
const rendered = map.queryRenderedFeatures(e.point, { layers: assetLayerIds });
|
|
||||||
if (rendered && rendered.length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!e.features || !e.features[0]) return;
|
|
||||||
const props = e.features[0].properties || {};
|
|
||||||
|
|
||||||
let typeHtml = '';
|
|
||||||
if (props.zone_type_name) {
|
|
||||||
typeHtml = `<span class="badge me-1" style="background-color: ${props.color || '#198754'}; color: #fff; font-size: 10px;">
|
|
||||||
${props.zone_type_name}
|
|
||||||
</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let freqHtml = '';
|
|
||||||
if (props.target_frequency_days) {
|
|
||||||
freqHtml = `<div style="color: #64748b; font-size: 11px; margin-top: 3px;">
|
|
||||||
<i class="bi bi-arrow-repeat me-1"></i>${_t('Fréquence cible :', 'Fréquence cible :')} <strong>${props.target_frequency_days} ${_t('jours', 'jours')}</strong>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seasonHtml = '';
|
|
||||||
if (props.seasonality_notes) {
|
|
||||||
seasonHtml = `<div style="color: #64748b; font-size: 11px; margin-top: 3px;">
|
|
||||||
<i class="bi bi-calendar3 me-1"></i>${props.seasonality_notes}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let instructionsHtml = '';
|
|
||||||
if (props.instructions) {
|
|
||||||
instructionsHtml = `<div style="background-color: #f8fafc; border-left: 3px solid ${props.color || '#198754'}; padding: 6px; margin-top: 6px; font-size: 11px; color: #334155; border-radius: 2px; max-height: 100px; overflow-y: auto;">
|
|
||||||
<div style="font-weight: 600; margin-bottom: 2px;"><i class="bi bi-exclamation-triangle me-1"></i>${_t('Consignes & points d\'attention', 'Consignes & points d\'attention')}</div>
|
|
||||||
${props.instructions}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let descHtml = '';
|
|
||||||
if (props.description) {
|
|
||||||
descHtml = `<div style="color: #475569; font-size: 11px; margin-top: 4px;">
|
|
||||||
${props.description}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = `
|
|
||||||
<div style="font-family: inherit; font-size: 12px; padding: 2px;">
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 4px;">
|
|
||||||
<div style="display: flex; align-items: center; gap: 6px;">
|
|
||||||
<span style="display: inline-block; width: 12px; height: 12px; border-radius: 3px; background-color: ${props.color || '#198754'}; border: 1px solid rgba(0,0,0,0.2); flex-shrink: 0;"></span>
|
|
||||||
<strong style="font-size: 13px; color: #1e293b;">${props.code}</strong>
|
|
||||||
</div>
|
|
||||||
${typeHtml}
|
|
||||||
</div>
|
|
||||||
<div style="font-weight: 600; color: #1e293b; margin-bottom: 4px;">${props.name}</div>
|
|
||||||
${descHtml}
|
|
||||||
${freqHtml}
|
|
||||||
${seasonHtml}
|
|
||||||
${instructionsHtml}
|
|
||||||
<div style="margin-top: 8px; text-align: right;">
|
|
||||||
<a href="/assets/management-zones/${props.id}/" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 11px;">
|
|
||||||
<i class="bi bi-eye me-1"></i>${_t('Détails & gestion', 'Détails & gestion')} →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
zonePopup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on('mousemove', fillLayerId, (e) => {
|
|
||||||
const assetLayerIds = (window.assets_layers_defs || []).map(l => l.id).filter(id => map.getLayer(id));
|
|
||||||
if (assetLayerIds.length > 0) {
|
|
||||||
const rendered = map.queryRenderedFeatures(e.point, { layers: assetLayerIds });
|
|
||||||
if (rendered && rendered.length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
map.getCanvas().style.cursor = 'pointer';
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on('mouseleave', fillLayerId, () => {
|
|
||||||
map.getCanvas().style.cursor = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Intégration dans le LayerSwitcherControl
|
|
||||||
if (layerSwitcher && typeof layerSwitcher.addCustomSection === 'function') {
|
|
||||||
layerSwitcher.addCustomSection({
|
|
||||||
id: 'management-zones',
|
|
||||||
title: `<span class="d-flex align-items-center gap-1"><i class="bi bi-bounding-box-circles me-1"></i>${_t('Zones de gestion', 'Zones de gestion')}</span>`,
|
|
||||||
render: (container, switcher) => {
|
|
||||||
renderManagementZonesSection(container, switcher, features, map, fillLayerId, lineLayerId, zonePopup, _t, thematic);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (features.length > 0) {
|
|
||||||
layerSwitcher.setOverlayActive(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[ManagementZonesMapOverlay] Error initializing overlay:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rendu HTML et interactions de la section "Zones de gestion" dans le LayerSwitcherControl.
|
|
||||||
*/
|
|
||||||
function renderManagementZonesSection(container, switcher, features, map, fillLayerId, lineLayerId, zonePopup, _t, thematic) {
|
|
||||||
if (!features || features.length === 0) {
|
|
||||||
const manageUrl = `/assets/management-zones/${thematic ? '?thematic=' + thematic : ''}`;
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="text-muted small px-2 py-1 fst-italic d-flex justify-content-between align-items-center">
|
|
||||||
<span>${_t('Aucune zone de gestion définie pour cette thématique.', 'Aucune zone de gestion définie pour cette thématique.')}</span>
|
|
||||||
<a href="${manageUrl}" class="btn btn-link btn-sm p-0 text-muted ms-1" title="${_t('Gérer les zones de gestion', 'Gérer les zones de gestion')}">
|
|
||||||
<i class="bi bi-box-arrow-up-right"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const countText = `${features.length} ${_t('zones', 'zones')}`;
|
|
||||||
const manageUrl = `/assets/management-zones/${thematic ? '?thematic=' + thematic : ''}`;
|
|
||||||
|
|
||||||
// En-tête avec toggle général
|
|
||||||
const headerEl = document.createElement('div');
|
|
||||||
headerEl.className = 'd-flex justify-content-between align-items-center px-1 mb-2';
|
|
||||||
headerEl.innerHTML = `
|
|
||||||
<div class="d-flex align-items-center gap-1">
|
|
||||||
<span class="text-muted small fw-semibold">${countText}</span>
|
|
||||||
<a href="${manageUrl}" class="btn btn-link btn-sm p-0 text-muted" style="font-size: 11px;" title="${_t('Gérer les zones de gestion', 'Gérer les zones de gestion')}">
|
|
||||||
<i class="bi bi-box-arrow-up-right"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-switch mb-0">
|
|
||||||
<input class="form-check-input management-zones-master-toggle" type="checkbox" role="switch" checked title="${_t('Afficher / masquer toutes les zones', 'Afficher / masquer toutes les zones')}">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(headerEl);
|
|
||||||
|
|
||||||
const masterToggle = headerEl.querySelector('.management-zones-master-toggle');
|
|
||||||
|
|
||||||
// Curseur d'opacité
|
|
||||||
const opacityRow = document.createElement('div');
|
|
||||||
opacityRow.className = 'px-1 mb-2';
|
|
||||||
opacityRow.innerHTML = `
|
|
||||||
<div class="d-flex justify-content-between align-items-center small text-muted mb-1">
|
|
||||||
<span>${_t('Opacité', 'Opacité')}</span>
|
|
||||||
<span class="management-zones-opacity-val">20%</span>
|
|
||||||
</div>
|
|
||||||
<input type="range" class="form-range management-zones-opacity-slider" min="5" max="80" value="20" style="height: 4px;">
|
|
||||||
`;
|
|
||||||
container.appendChild(opacityRow);
|
|
||||||
|
|
||||||
const opacitySlider = opacityRow.querySelector('.management-zones-opacity-slider');
|
|
||||||
const opacityValLabel = opacityRow.querySelector('.management-zones-opacity-val');
|
|
||||||
|
|
||||||
opacitySlider.addEventListener('input', (e) => {
|
|
||||||
const val = parseInt(e.target.value, 10);
|
|
||||||
opacityValLabel.textContent = `${val}%`;
|
|
||||||
if (map.getLayer(fillLayerId)) {
|
|
||||||
map.setPaintProperty(fillLayerId, 'fill-opacity', val / 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Liste des zones
|
|
||||||
const listEl = document.createElement('div');
|
|
||||||
listEl.className = 'management-zones-list d-flex flex-column gap-1';
|
|
||||||
listEl.style.maxHeight = '240px';
|
|
||||||
listEl.style.overflowY = 'auto';
|
|
||||||
|
|
||||||
const activeZoneIds = new Set(features.map(f => f.id));
|
|
||||||
|
|
||||||
function updateLayersVisibility() {
|
|
||||||
if (!masterToggle.checked || activeZoneIds.size === 0) {
|
|
||||||
if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, 'visibility', 'none');
|
|
||||||
if (map.getLayer(lineLayerId)) map.setLayoutProperty(lineLayerId, 'visibility', 'none');
|
|
||||||
} else {
|
|
||||||
if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, 'visibility', 'visible');
|
|
||||||
if (map.getLayer(lineLayerId)) map.setLayoutProperty(lineLayerId, 'visibility', 'visible');
|
|
||||||
|
|
||||||
const filter = ['in', ['id'], ['literal', Array.from(activeZoneIds)]];
|
|
||||||
if (map.getLayer(fillLayerId)) map.setFilter(fillLayerId, filter);
|
|
||||||
if (map.getLayer(lineLayerId)) map.setFilter(lineLayerId, filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
masterToggle.addEventListener('change', () => {
|
|
||||||
const checked = masterToggle.checked;
|
|
||||||
listEl.querySelectorAll('.zone-item-toggle').forEach(chk => {
|
|
||||||
chk.checked = checked;
|
|
||||||
});
|
|
||||||
if (checked) {
|
|
||||||
features.forEach(f => activeZoneIds.add(f.id));
|
|
||||||
} else {
|
|
||||||
activeZoneIds.clear();
|
|
||||||
}
|
|
||||||
updateLayersVisibility();
|
|
||||||
});
|
|
||||||
|
|
||||||
features.forEach(feat => {
|
|
||||||
const props = feat.properties || {};
|
|
||||||
const color = props.color || '#198754';
|
|
||||||
const itemEl = document.createElement('div');
|
|
||||||
itemEl.className = 'd-flex align-items-center justify-content-between p-1 rounded hover-bg small';
|
|
||||||
itemEl.style.cursor = 'pointer';
|
|
||||||
|
|
||||||
itemEl.innerHTML = `
|
|
||||||
<div class="d-flex align-items-center gap-2 text-truncate me-2 zone-click-target" style="flex: 1;">
|
|
||||||
<span style="display:inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${color}; border: 1px solid rgba(0,0,0,0.25); flex-shrink: 0;"></span>
|
|
||||||
<div class="text-truncate">
|
|
||||||
<div class="fw-semibold text-truncate text-dark" style="font-size: 11px;">${props.code}</div>
|
|
||||||
<div class="text-muted text-truncate" style="font-size: 10px;">${props.name} ${props.zone_type_name ? `(${props.zone_type_name})` : ''}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center gap-1 flex-shrink-0">
|
|
||||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted zoom-btn" title="${_t('Centrer sur la zone', 'Centrer sur la zone')}">
|
|
||||||
<i class="bi bi-geo-alt" style="font-size: 12px;"></i>
|
|
||||||
</button>
|
|
||||||
<div class="form-check form-switch mb-0 ms-1">
|
|
||||||
<input class="form-check-input zone-item-toggle" type="checkbox" role="switch" checked data-zone-id="${feat.id}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const itemToggle = itemEl.querySelector('.zone-item-toggle');
|
|
||||||
itemToggle.addEventListener('change', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (itemToggle.checked) {
|
|
||||||
activeZoneIds.add(feat.id);
|
|
||||||
} else {
|
|
||||||
activeZoneIds.delete(feat.id);
|
|
||||||
}
|
|
||||||
masterToggle.checked = (activeZoneIds.size > 0);
|
|
||||||
updateLayersVisibility();
|
|
||||||
});
|
|
||||||
|
|
||||||
function zoomToZone() {
|
|
||||||
const bbox = getGeometryBBox(feat.geometry);
|
|
||||||
if (bbox) {
|
|
||||||
map.fitBounds(bbox, { padding: 40, maxZoom: 16 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemEl.querySelector('.zoom-btn').addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
zoomToZone();
|
|
||||||
});
|
|
||||||
|
|
||||||
itemEl.querySelector('.zone-click-target').addEventListener('click', () => {
|
|
||||||
zoomToZone();
|
|
||||||
});
|
|
||||||
|
|
||||||
listEl.appendChild(itemEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(listEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.initManagementZonesMapOverlay = initManagementZonesMapOverlay;
|
|
||||||
|
|
||||||
})(window);
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
|
||||||
<defs>
|
|
||||||
<!-- Background grass blades gradient (deep rich green) -->
|
|
||||||
<linearGradient id="grass-dark" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#2e7d32"/>
|
|
||||||
<stop offset="100%" stop-color="#1b5e20"/>
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
<!-- Mid grass blades gradient (fresh vibrant green) -->
|
|
||||||
<linearGradient id="grass-mid" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#66bb6a"/>
|
|
||||||
<stop offset="100%" stop-color="#2e7d32"/>
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
<!-- Side arching grass blades gradient -->
|
|
||||||
<linearGradient id="grass-light" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#81c784"/>
|
|
||||||
<stop offset="100%" stop-color="#388e3c"/>
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
<!-- Ground turf base gradient -->
|
|
||||||
<linearGradient id="turf-base" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#43a047"/>
|
|
||||||
<stop offset="100%" stop-color="#1b5e20"/>
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
<!-- Front turf lip gradient -->
|
|
||||||
<linearGradient id="turf-front" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#4caf50"/>
|
|
||||||
<stop offset="100%" stop-color="#2e7d32"/>
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
<!-- Soil ground base gradient -->
|
|
||||||
<linearGradient id="soil-base" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#6d4c41"/>
|
|
||||||
<stop offset="100%" stop-color="#4e342e"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Ground / Soil base slab -->
|
|
||||||
<rect x="9" y="52" width="46" height="6" rx="3" fill="url(#soil-base)"/>
|
|
||||||
|
|
||||||
<!-- Back turf mound -->
|
|
||||||
<ellipse cx="32" cy="49" rx="25" ry="7" fill="url(#turf-base)"/>
|
|
||||||
|
|
||||||
<!-- Background grass blades -->
|
|
||||||
<!-- Mid-left tall blade -->
|
|
||||||
<path d="M 21 48 C 17 32, 16 19, 20 11 C 24 15, 27 29, 28 48 Z" fill="url(#grass-dark)"/>
|
|
||||||
<!-- Mid-right tall blade -->
|
|
||||||
<path d="M 36 48 C 37 29, 40 15, 44 11 C 48 19, 47 32, 43 48 Z" fill="url(#grass-dark)"/>
|
|
||||||
|
|
||||||
<!-- Main foreground grass blades -->
|
|
||||||
<!-- Left arching blade -->
|
|
||||||
<path d="M 14 49 C 10 38, 7 28, 8 19 C 14 21, 21 34, 25 49 Z" fill="url(#grass-light)"/>
|
|
||||||
<!-- Right arching blade -->
|
|
||||||
<path d="M 39 49 C 43 34, 50 21, 56 19 C 57 28, 54 38, 50 49 Z" fill="url(#grass-light)"/>
|
|
||||||
<!-- Center tall proud blade -->
|
|
||||||
<path d="M 26 49 C 27 30, 28 16, 32 6 C 36 16, 37 30, 38 49 Z" fill="url(#grass-mid)"/>
|
|
||||||
|
|
||||||
<!-- Center blade subtle highlight -->
|
|
||||||
<path d="M 31 48 C 31.5 32, 31.8 18, 32 9 C 32.2 18, 32.8 32, 32.5 48 Z" fill="#a5d6a7" opacity="0.6"/>
|
|
||||||
|
|
||||||
<!-- Foreground turf rim -->
|
|
||||||
<ellipse cx="32" cy="51" rx="23" ry="5" fill="url(#turf-front)"/>
|
|
||||||
|
|
||||||
<!-- Small front sprouting blades -->
|
|
||||||
<path d="M 21 51 C 20 44, 18 36, 17 30 C 20 34, 23 42, 24 51 Z" fill="url(#grass-light)"/>
|
|
||||||
<path d="M 40 51 C 41 42, 44 34, 47 30 C 46 36, 44 44, 43 51 Z" fill="url(#grass-light)"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
|
@ -1,393 +0,0 @@
|
||||||
/**
|
|
||||||
* Visualisation cartographique détaillée d'une localisation Nature (StreetUp / Loko)
|
|
||||||
*/
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
const mapContainer = document.getElementById('map');
|
|
||||||
if (!mapContainer) return;
|
|
||||||
|
|
||||||
const lon = typeof feature_lon !== 'undefined' && feature_lon !== null ? feature_lon : 4.3517;
|
|
||||||
const lat = typeof feature_lat !== 'undefined' && feature_lat !== null ? feature_lat : 50.8503;
|
|
||||||
|
|
||||||
const map = new maplibregl.Map({
|
|
||||||
container: 'map',
|
|
||||||
style: window.MAP_STYLE_LIGHT || 'https://api.maptiler.com/maps/streets-v2-light/style.json',
|
|
||||||
center: [lon, lat],
|
|
||||||
zoom: 16,
|
|
||||||
pitch: 35,
|
|
||||||
bearing: -10,
|
|
||||||
antialias: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Helper to place raster basemaps below nature layers ──
|
|
||||||
function getBasemapBeforeLayer(m) {
|
|
||||||
if (!m || !m.getStyle) return undefined;
|
|
||||||
if (m.getLayer('nature-location-fill')) return 'nature-location-fill';
|
|
||||||
if (m.getLayer('nature-location-outline')) return 'nature-location-outline';
|
|
||||||
if (m.getLayer('layer-surfaces-fill')) return 'layer-surfaces-fill';
|
|
||||||
if (m.getLayer('layer-surfaces-outline')) return 'layer-surfaces-outline';
|
|
||||||
if (m.getLayer('layer-shrubs')) return 'layer-shrubs';
|
|
||||||
if (m.getLayer('layer-benches')) return 'layer-benches';
|
|
||||||
if (m.getLayer('layer-trees')) return 'layer-trees';
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Ensure strict layer stacking order on top of basemaps ──
|
|
||||||
function reorderNatureLayers() {
|
|
||||||
const layerOrder = [
|
|
||||||
'nature-location-fill',
|
|
||||||
'nature-location-outline',
|
|
||||||
'layer-surfaces-fill',
|
|
||||||
'layer-surfaces-outline',
|
|
||||||
'layer-shrubs',
|
|
||||||
'layer-benches',
|
|
||||||
'layer-trees',
|
|
||||||
];
|
|
||||||
layerOrder.forEach(function (layerId) {
|
|
||||||
if (map.getLayer(layerId)) {
|
|
||||||
try {
|
|
||||||
map.moveLayer(layerId);
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add layer switcher control for basemaps / orthophotos
|
|
||||||
if (typeof LayerSwitcherControl !== 'undefined') {
|
|
||||||
const layerSwitcher = new LayerSwitcherControl({
|
|
||||||
layers: [{
|
|
||||||
id: 'basemap',
|
|
||||||
name: 'Fonds de plan & Orthophotos',
|
|
||||||
icon: 'satellite',
|
|
||||||
group: 'basemap',
|
|
||||||
beforeLayer: getBasemapBeforeLayer,
|
|
||||||
onLayerAdded: () => reorderNatureLayers(),
|
|
||||||
onVisibilityChange: () => reorderNatureLayers(),
|
|
||||||
layer: {
|
|
||||||
type: 'raster',
|
|
||||||
paint: { 'raster-opacity': 1 }
|
|
||||||
},
|
|
||||||
basemaps: (window.MAP_BACKGROUNDS && window.MAP_BACKGROUNDS.length > 0) ? window.MAP_BACKGROUNDS : undefined,
|
|
||||||
sublayers: [
|
|
||||||
{
|
|
||||||
id: 'urbis',
|
|
||||||
name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)',
|
|
||||||
default: true,
|
|
||||||
source: {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: [
|
|
||||||
window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}'
|
|
||||||
],
|
|
||||||
tileSize: 256,
|
|
||||||
attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'vlaanderen',
|
|
||||||
name: 'Vlaanderen 2023',
|
|
||||||
source: {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: [
|
|
||||||
'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'
|
|
||||||
],
|
|
||||||
tileSize: 256,
|
|
||||||
attribution: '© Digitaal Vlaanderen - Orthofoto 2023'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
map.addControl(layerSwitcher, 'top-right');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add navigation and fullscreen controls
|
|
||||||
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), 'top-right');
|
|
||||||
map.addControl(new maplibregl.FullscreenControl(), 'top-right');
|
|
||||||
|
|
||||||
map.on('load', function () {
|
|
||||||
let combinedBbox = null;
|
|
||||||
|
|
||||||
// 1. Location geometry (polygon / boundary)
|
|
||||||
if (typeof section_geojson !== 'undefined' && section_geojson) {
|
|
||||||
const locGeom = typeof section_geojson === 'string' ? JSON.parse(section_geojson) : section_geojson;
|
|
||||||
const locationFeature = {
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: locGeom,
|
|
||||||
properties: {
|
|
||||||
name: 'Location'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
map.addSource('nature-location', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: locationFeature
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fill layer for the section
|
|
||||||
map.addLayer({
|
|
||||||
id: 'nature-location-fill',
|
|
||||||
type: 'fill',
|
|
||||||
source: 'nature-location',
|
|
||||||
paint: {
|
|
||||||
'fill-color': '#16a34a',
|
|
||||||
'fill-opacity': 0.15
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Outline layer for the section
|
|
||||||
map.addLayer({
|
|
||||||
id: 'nature-location-outline',
|
|
||||||
type: 'line',
|
|
||||||
source: 'nature-location',
|
|
||||||
paint: {
|
|
||||||
'line-color': '#15803d',
|
|
||||||
'line-width': 2.5
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof turf !== 'undefined') {
|
|
||||||
try {
|
|
||||||
combinedBbox = turf.bbox(locationFeature);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Could not compute location bbox:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Assets (green surfaces, trees, shrubs, benches)
|
|
||||||
if (typeof assets_geojson !== 'undefined' && assets_geojson) {
|
|
||||||
const assetsData = typeof assets_geojson === 'string' ? JSON.parse(assets_geojson) : assets_geojson;
|
|
||||||
|
|
||||||
map.addSource('nature-assets', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: assetsData
|
|
||||||
});
|
|
||||||
|
|
||||||
// Green surfaces fill
|
|
||||||
map.addLayer({
|
|
||||||
id: 'layer-surfaces-fill',
|
|
||||||
type: 'fill',
|
|
||||||
source: 'nature-assets',
|
|
||||||
filter: ['==', ['get', 'asset_type'], 'naturegreensurface'],
|
|
||||||
paint: {
|
|
||||||
'fill-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
|
||||||
'fill-opacity': 0.65
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Green surfaces outline
|
|
||||||
map.addLayer({
|
|
||||||
id: 'layer-surfaces-outline',
|
|
||||||
type: 'line',
|
|
||||||
source: 'nature-assets',
|
|
||||||
filter: ['==', ['get', 'asset_type'], 'naturegreensurface'],
|
|
||||||
paint: {
|
|
||||||
'line-color': '#166534',
|
|
||||||
'line-width': 1.5
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Trees (circles)
|
|
||||||
map.addLayer({
|
|
||||||
id: 'layer-trees',
|
|
||||||
type: 'circle',
|
|
||||||
source: 'nature-assets',
|
|
||||||
filter: ['==', ['get', 'asset_type'], 'naturetree'],
|
|
||||||
paint: {
|
|
||||||
'circle-radius': [
|
|
||||||
'interpolate', ['linear'], ['zoom'],
|
|
||||||
13, 4,
|
|
||||||
16, 7,
|
|
||||||
19, 12
|
|
||||||
],
|
|
||||||
'circle-color': '#15803d',
|
|
||||||
'circle-stroke-width': 2,
|
|
||||||
'circle-stroke-color': '#ffffff'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Shrubs (circles)
|
|
||||||
map.addLayer({
|
|
||||||
id: 'layer-shrubs',
|
|
||||||
type: 'circle',
|
|
||||||
source: 'nature-assets',
|
|
||||||
filter: ['==', ['get', 'asset_type'], 'natureshrub'],
|
|
||||||
paint: {
|
|
||||||
'circle-radius': [
|
|
||||||
'interpolate', ['linear'], ['zoom'],
|
|
||||||
13, 3,
|
|
||||||
16, 5,
|
|
||||||
19, 9
|
|
||||||
],
|
|
||||||
'circle-color': '#84cc16',
|
|
||||||
'circle-stroke-width': 1.5,
|
|
||||||
'circle-stroke-color': '#ffffff'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Benches (circles)
|
|
||||||
map.addLayer({
|
|
||||||
id: 'layer-benches',
|
|
||||||
type: 'circle',
|
|
||||||
source: 'nature-assets',
|
|
||||||
filter: ['==', ['get', 'asset_type'], 'naturebench'],
|
|
||||||
paint: {
|
|
||||||
'circle-radius': [
|
|
||||||
'interpolate', ['linear'], ['zoom'],
|
|
||||||
13, 3,
|
|
||||||
16, 5,
|
|
||||||
19, 9
|
|
||||||
],
|
|
||||||
'circle-color': '#b45309',
|
|
||||||
'circle-stroke-width': 1.5,
|
|
||||||
'circle-stroke-color': '#ffffff'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fit bounds with Turf if features exist
|
|
||||||
if (typeof turf !== 'undefined' && assetsData.features && assetsData.features.length > 0) {
|
|
||||||
try {
|
|
||||||
const assetsBbox = turf.bbox(assetsData);
|
|
||||||
if (!combinedBbox) {
|
|
||||||
combinedBbox = assetsBbox;
|
|
||||||
} else {
|
|
||||||
combinedBbox = [
|
|
||||||
Math.min(combinedBbox[0], assetsBbox[0]),
|
|
||||||
Math.min(combinedBbox[1], assetsBbox[1]),
|
|
||||||
Math.max(combinedBbox[2], assetsBbox[2]),
|
|
||||||
Math.max(combinedBbox[3], assetsBbox[3])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Could not compute assets bbox:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply fit bounds
|
|
||||||
if (combinedBbox && combinedBbox[0] !== Infinity) {
|
|
||||||
map.fitBounds(combinedBbox, {
|
|
||||||
padding: { top: 40, bottom: 40, left: 40, right: 40 },
|
|
||||||
maxZoom: 18,
|
|
||||||
duration: 800
|
|
||||||
});
|
|
||||||
}
|
|
||||||
reorderNatureLayers();
|
|
||||||
|
|
||||||
// 3. Interactive Popups
|
|
||||||
const interactiveLayers = [
|
|
||||||
'layer-surfaces-fill',
|
|
||||||
'layer-trees',
|
|
||||||
'layer-shrubs',
|
|
||||||
'layer-benches'
|
|
||||||
];
|
|
||||||
|
|
||||||
interactiveLayers.forEach(function (layerId) {
|
|
||||||
map.on('mouseenter', layerId, function () {
|
|
||||||
map.getCanvas().style.cursor = 'pointer';
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on('mouseleave', layerId, function () {
|
|
||||||
map.getCanvas().style.cursor = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on('click', layerId, function (e) {
|
|
||||||
if (!e.features || !e.features.length) return;
|
|
||||||
const f = e.features[0];
|
|
||||||
const props = f.properties;
|
|
||||||
|
|
||||||
let iconClass = 'bi-tree-fill text-success';
|
|
||||||
let typeBadge = '<span class="badge bg-success">Surface</span>';
|
|
||||||
|
|
||||||
if (props.asset_type === 'naturetree') {
|
|
||||||
iconClass = 'bi-tree-fill text-success';
|
|
||||||
typeBadge = '<span class="badge bg-success">Arbre</span>';
|
|
||||||
} else if (props.asset_type === 'natureshrub') {
|
|
||||||
iconClass = 'bi-flower3 text-success';
|
|
||||||
typeBadge = '<span class="badge bg-info">Arbuste</span>';
|
|
||||||
} else if (props.asset_type === 'naturebench') {
|
|
||||||
iconClass = 'bi-inbox text-warning';
|
|
||||||
typeBadge = '<span class="badge bg-warning text-dark">Banc / Mobilier</span>';
|
|
||||||
} else if (props.asset_type === 'naturegreensurface') {
|
|
||||||
iconClass = 'bi-grid-fill text-success';
|
|
||||||
typeBadge = '<span class="badge bg-primary">Surface verte</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
let detailsHtml = '';
|
|
||||||
if (props.area) {
|
|
||||||
detailsHtml += `<div class="small text-muted"><i class="bi bi-rulers me-1"></i>Superficie : <strong>${props.area}</strong></div>`;
|
|
||||||
}
|
|
||||||
if (props.vitality) {
|
|
||||||
detailsHtml += `<div class="small text-muted"><i class="bi bi-heart-pulse me-1"></i>Vitalité : <strong>${props.vitality}</strong></div>`;
|
|
||||||
}
|
|
||||||
if (props.stage) {
|
|
||||||
detailsHtml += `<div class="small text-muted"><i class="bi bi-clock-history me-1"></i>Stade : <strong>${props.stage}</strong></div>`;
|
|
||||||
}
|
|
||||||
if (props.height) {
|
|
||||||
detailsHtml += `<div class="small text-muted"><i class="bi bi-arrows-vertical me-1"></i>Hauteur : <strong>${props.height}</strong></div>`;
|
|
||||||
}
|
|
||||||
if (props.is_remarkable === 'true' || props.is_remarkable === true) {
|
|
||||||
detailsHtml += `<div class="badge bg-warning text-dark my-1"><i class="bi bi-star-fill me-1"></i>Arbre remarquable</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const popupHtml = `
|
|
||||||
<div class="nature-map-popup" style="min-width: 200px; padding: 4px;">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1 pb-1 border-bottom">
|
|
||||||
<span class="fw-bold"><i class="bi ${iconClass} me-1"></i>${props.code || ''}</span>
|
|
||||||
${typeBadge}
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
${props.model ? `<div class="fw-semibold text-dark">${props.model}</div>` : ''}
|
|
||||||
${props.scientific_name ? `<div class="fst-italic text-muted small">${props.scientific_name}</div>` : ''}
|
|
||||||
${props.status ? `<div class="small"><span class="badge bg-light text-secondary border mt-1">${props.status}</span></div>` : ''}
|
|
||||||
${detailsHtml}
|
|
||||||
</div>
|
|
||||||
${props.detail_url ? `
|
|
||||||
<a href="${props.detail_url}" class="btn btn-xs btn-primary w-100 text-center py-1" style="font-size: 0.8rem;">
|
|
||||||
<i class="bi bi-arrow-right-circle me-1"></i>Consulter la fiche
|
|
||||||
</a>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
new maplibregl.Popup({ closeButton: true, closeOnClick: true, maxWidth: '280px' })
|
|
||||||
.setLngLat(e.lngLat)
|
|
||||||
.setHTML(popupHtml)
|
|
||||||
.addTo(map);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Layer Toggle Handlers
|
|
||||||
window.toggleNatureLayer = function (layerGroup, isChecked) {
|
|
||||||
const layerMapping = {
|
|
||||||
'surfaces': ['layer-surfaces-fill', 'layer-surfaces-outline'],
|
|
||||||
'trees': ['layer-trees'],
|
|
||||||
'shrubs': ['layer-shrubs'],
|
|
||||||
'benches': ['layer-benches']
|
|
||||||
};
|
|
||||||
|
|
||||||
const layers = layerMapping[layerGroup];
|
|
||||||
if (layers) {
|
|
||||||
const visibility = isChecked ? 'visible' : 'none';
|
|
||||||
layers.forEach(function (id) {
|
|
||||||
if (map.getLayer(id)) {
|
|
||||||
map.setLayoutProperty(id, 'visibility', visibility);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 5. Global Zoom to Asset helper
|
|
||||||
window.zoomToAsset = function (assetLon, assetLat, assetCode) {
|
|
||||||
if (assetLon && assetLat) {
|
|
||||||
map.flyTo({
|
|
||||||
center: [assetLon, assetLat],
|
|
||||||
zoom: 18.5,
|
|
||||||
pitch: 45,
|
|
||||||
duration: 1000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="foliage-dead" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#9ca3af"/>
|
|
||||||
<stop offset="100%" stop-color="#4b5563"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="trunk-dead" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#78716c"/>
|
|
||||||
<stop offset="100%" stop-color="#57534e"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-dead)"/>
|
|
||||||
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-dead)"/>
|
|
||||||
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-dead)"/>
|
|
||||||
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-dead)"/>
|
|
||||||
|
|
||||||
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk-dead)"/>
|
|
||||||
<rect x="25" y="54" width="14" height="5" rx="2" fill="#44403c"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 928 B |
|
|
@ -1,20 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="foliage-reg" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#60a5fa"/>
|
|
||||||
<stop offset="100%" stop-color="#1d4ed8"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="trunk" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#8d6e63"/>
|
|
||||||
<stop offset="100%" stop-color="#5d4037"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-reg)"/>
|
|
||||||
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-reg)"/>
|
|
||||||
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-reg)"/>
|
|
||||||
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-reg)"/>
|
|
||||||
|
|
||||||
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk)"/>
|
|
||||||
<rect x="25" y="54" width="14" height="5" rx="2" fill="#4e342e"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 913 B |
|
|
@ -1,20 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="foliage-rem" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#fbbf24"/>
|
|
||||||
<stop offset="100%" stop-color="#d97706"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="trunk" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#8d6e63"/>
|
|
||||||
<stop offset="100%" stop-color="#5d4037"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-rem)"/>
|
|
||||||
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-rem)"/>
|
|
||||||
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-rem)"/>
|
|
||||||
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-rem)"/>
|
|
||||||
|
|
||||||
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk)"/>
|
|
||||||
<rect x="25" y="54" width="14" height="5" rx="2" fill="#4e342e"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 913 B |
|
|
@ -19,15 +19,7 @@ var activeCategories = new Set(SELECTED_CATEGORIES || []);
|
||||||
var activeModels = new Set();
|
var activeModels = new Set();
|
||||||
var modelFilterCollapsed = true;
|
var modelFilterCollapsed = true;
|
||||||
var activeConditionalFilters = {};
|
var activeConditionalFilters = {};
|
||||||
var currentTypeFilter = 'all'; // legacy fallback
|
var currentTypeFilter = 'all'; // 'all' | 'locations' | 'assets'
|
||||||
|
|
||||||
function getActiveTypeFilters() {
|
|
||||||
const types = [];
|
|
||||||
if (document.getElementById('type-cb-assets')?.checked) types.push('assets');
|
|
||||||
if (document.getElementById('type-cb-locations')?.checked) types.push('locations');
|
|
||||||
if (document.getElementById('type-cb-management-zones')?.checked) types.push('management_zones');
|
|
||||||
return types;
|
|
||||||
}
|
|
||||||
var searchTerms = [];
|
var searchTerms = [];
|
||||||
var searchMode = 'and';
|
var searchMode = 'and';
|
||||||
var shouldFitSearch = false;
|
var shouldFitSearch = false;
|
||||||
|
|
@ -53,7 +45,6 @@ var assetsLoadModes = {}; // thematic_code → null | 'always' | 'filter' |
|
||||||
|
|
||||||
// MapLibre sources
|
// MapLibre sources
|
||||||
var mergedLocationsGeojson = { type: 'FeatureCollection', features: [] };
|
var mergedLocationsGeojson = { type: 'FeatureCollection', features: [] };
|
||||||
var mergedManagementZonesGeojson = { type: 'FeatureCollection', features: [] };
|
|
||||||
var mergedAssetsGeojson = { type: 'FeatureCollection', features: [] };
|
var mergedAssetsGeojson = { type: 'FeatureCollection', features: [] };
|
||||||
var currentFetchAbortController = null;
|
var currentFetchAbortController = null;
|
||||||
var assetClickPopup = null;
|
var assetClickPopup = null;
|
||||||
|
|
@ -535,7 +526,7 @@ function buildColumns() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'element_type', title: gettext('Type'), sortable: true, width: 145, widthUnit: 'px', valign: 'middle',
|
field: 'element_type', title: gettext('Type'), sortable: true, width: 130, widthUnit: 'px', valign: 'middle',
|
||||||
formatter: function (v, row) {
|
formatter: function (v, row) {
|
||||||
const table = $('#unified-table');
|
const table = $('#unified-table');
|
||||||
let isCatVisible = false;
|
let isCatVisible = false;
|
||||||
|
|
@ -543,21 +534,10 @@ function buildColumns() {
|
||||||
isCatVisible = table.length && typeof table.bootstrapTable === 'function' &&
|
isCatVisible = table.length && typeof table.bootstrapTable === 'function' &&
|
||||||
table.bootstrapTable('getVisibleColumns').some(c => c.field === 'category');
|
table.bootstrapTable('getVisibleColumns').some(c => c.field === 'category');
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
let iconPrefix = '';
|
|
||||||
if (typeof AssetSymbology !== 'undefined' && row && !row.is_location && !row.is_management_zone) {
|
|
||||||
const symb = AssetSymbology.getAssetSymbology(row);
|
|
||||||
const colorStyle = symb.color ? `color:${symb.color};` : '';
|
|
||||||
iconPrefix = AssetSymbology.renderIconHtml(symb, {
|
|
||||||
style: `${colorStyle} width: 20px; height: 20px; vertical-align: middle; margin-right: 6px;`,
|
|
||||||
imgClass: 'me-1'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCatVisible && row && row.is_location && row.category) {
|
if (!isCatVisible && row && row.is_location && row.category) {
|
||||||
return `${iconPrefix}${escapeHtml(v || '-')} <span class="badge bg-light text-secondary border fw-normal ms-1" style="font-size:0.75rem;">${escapeHtml(row.category)}</span>`;
|
return `${escapeHtml(v || '-')} <span class="badge bg-light text-secondary border fw-normal ms-1" style="font-size:0.75rem;">${escapeHtml(row.category)}</span>`;
|
||||||
}
|
}
|
||||||
return `${iconPrefix}${escapeHtml(v || '-')}`;
|
return escapeHtml(v || '-');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ field: 'thematic_name', title: gettext('Thématique'), sortable: true, width: 140, widthUnit: 'px', valign: 'middle' },
|
{ field: 'thematic_name', title: gettext('Thématique'), sortable: true, width: 140, widthUnit: 'px', valign: 'middle' },
|
||||||
|
|
@ -678,31 +658,9 @@ function applyInitialPreferences() {
|
||||||
|
|
||||||
// Type filter
|
// Type filter
|
||||||
if (prefs.type_filter) {
|
if (prefs.type_filter) {
|
||||||
if (Array.isArray(prefs.type_filter)) {
|
currentTypeFilter = prefs.type_filter;
|
||||||
const cbA = document.getElementById('type-cb-assets');
|
const rb = document.querySelector(`input[name="unified-type-filter"][value="${prefs.type_filter}"]`);
|
||||||
const cbL = document.getElementById('type-cb-locations');
|
if (rb) rb.checked = true;
|
||||||
const cbZ = document.getElementById('type-cb-management-zones');
|
|
||||||
if (cbA) cbA.checked = prefs.type_filter.includes('assets');
|
|
||||||
if (cbL) cbL.checked = prefs.type_filter.includes('locations');
|
|
||||||
if (cbZ) cbZ.checked = prefs.type_filter.includes('management_zones');
|
|
||||||
} else if (typeof prefs.type_filter === 'string') {
|
|
||||||
const cbA = document.getElementById('type-cb-assets');
|
|
||||||
const cbL = document.getElementById('type-cb-locations');
|
|
||||||
const cbZ = document.getElementById('type-cb-management-zones');
|
|
||||||
if (prefs.type_filter === 'all') {
|
|
||||||
if (cbA) cbA.checked = true;
|
|
||||||
if (cbL) cbL.checked = true;
|
|
||||||
if (cbZ) cbZ.checked = true;
|
|
||||||
} else if (prefs.type_filter === 'locations') {
|
|
||||||
if (cbA) cbA.checked = false;
|
|
||||||
if (cbL) cbL.checked = true;
|
|
||||||
if (cbZ) cbZ.checked = false;
|
|
||||||
} else if (prefs.type_filter === 'assets') {
|
|
||||||
if (cbA) cbA.checked = true;
|
|
||||||
if (cbL) cbL.checked = false;
|
|
||||||
if (cbZ) cbZ.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include no geometry filter
|
// Include no geometry filter
|
||||||
|
|
@ -748,14 +706,6 @@ function applyInitialPreferences() {
|
||||||
window._restoredMapCenter = prefs.map_center;
|
window._restoredMapCenter = prefs.map_center;
|
||||||
window._restoredMapZoom = prefs.map_zoom;
|
window._restoredMapZoom = prefs.map_zoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basemap preference
|
|
||||||
if (prefs.basemap) {
|
|
||||||
window._restoredBasemap = prefs.basemap;
|
|
||||||
try {
|
|
||||||
localStorage.setItem('loko_active_basemap', prefs.basemap);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
} else if (DEFAULT_THEMATIC_CODE && DEFAULT_THEMATIC_CODE in THEMATIC_CONFIG) {
|
} else if (DEFAULT_THEMATIC_CODE && DEFAULT_THEMATIC_CODE in THEMATIC_CONFIG) {
|
||||||
// First visit: pre-select the user's default thematic
|
// First visit: pre-select the user's default thematic
|
||||||
activeThematics = new Set([DEFAULT_THEMATIC_CODE]);
|
activeThematics = new Set([DEFAULT_THEMATIC_CODE]);
|
||||||
|
|
@ -783,22 +733,18 @@ function savePreferences() {
|
||||||
).map(cb => cb.value);
|
).map(cb => cb.value);
|
||||||
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
||||||
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
|
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
|
||||||
const activeBasemap = (window.unifiedLayerSwitcher && window.unifiedLayerSwitcher.activeBasemapId)
|
|
||||||
|| localStorage.getItem('loko_active_basemap')
|
|
||||||
|| 'default';
|
|
||||||
const prefs = {
|
const prefs = {
|
||||||
thematics: Array.from(activeThematics),
|
thematics: Array.from(activeThematics),
|
||||||
categories: Array.from(activeCategories),
|
categories: Array.from(activeCategories),
|
||||||
models: Array.from(activeModels),
|
models: Array.from(activeModels),
|
||||||
conditional_filters: activeConditionalFilters,
|
conditional_filters: activeConditionalFilters,
|
||||||
visible_columns: getVisibleColumnsPreference(),
|
visible_columns: getVisibleColumnsPreference(),
|
||||||
type_filter: getActiveTypeFilters(),
|
type_filter: currentTypeFilter,
|
||||||
status_filters: statusFilters,
|
status_filters: statusFilters,
|
||||||
validation_status_filters: validationStatusFilters,
|
validation_status_filters: validationStatusFilters,
|
||||||
include_no_geom: includeNoGeom,
|
include_no_geom: includeNoGeom,
|
||||||
map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null,
|
map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null,
|
||||||
map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null,
|
map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null,
|
||||||
basemap: activeBasemap,
|
|
||||||
};
|
};
|
||||||
fetch(SAVE_ASSETS_PREFS_URL, {
|
fetch(SAVE_ASSETS_PREFS_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -824,12 +770,6 @@ $(document).ready(function () {
|
||||||
|
|
||||||
$('#unified-table').bootstrapTable({
|
$('#unified-table').bootstrapTable({
|
||||||
columns: buildColumns(),
|
columns: buildColumns(),
|
||||||
rowStyle: function (row) {
|
|
||||||
if (row && (row.is_dead || row.vitality === 'dead' || row.status === 'dead' || row.status === 'archived')) {
|
|
||||||
return { classes: 'text-muted opacity-75' };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
height: calcHeight(),
|
height: calcHeight(),
|
||||||
search: false,
|
search: false,
|
||||||
pagination: true,
|
pagination: true,
|
||||||
|
|
@ -844,9 +784,9 @@ $(document).ready(function () {
|
||||||
onUncheckAll: function (rows) { rows.forEach(r => { r.state = false; selectedIds.delete(r.id); }); updateSelectionStatus(); },
|
onUncheckAll: function (rows) { rows.forEach(r => { r.state = false; selectedIds.delete(r.id); }); updateSelectionStatus(); },
|
||||||
onClickRow: function (row) {
|
onClickRow: function (row) {
|
||||||
if (!row.lon || !row.lat) return;
|
if (!row.lon || !row.lat) return;
|
||||||
const source = row.is_management_zone ? 'management_zones' : (row.is_location ? 'locations' : 'assets');
|
const source = row.is_location ? 'locations' : 'assets';
|
||||||
if (typeof highlightMapFeature === 'function') {
|
if (typeof highlightMapFeature === 'function') {
|
||||||
highlightMapFeature(map, source, row.id, row.is_management_zone ? managementZonesHighlightState : (row.is_location ? locationshighlightState : assetshighlightState));
|
highlightMapFeature(map, source, row.id, row.is_location ? locationshighlightState : assetshighlightState);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -925,13 +865,11 @@ $(document).ready(function () {
|
||||||
renderModelFilters();
|
renderModelFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Type checkboxes ────────────────────────────────────────────────────
|
// ─── Type radio ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
document.querySelectorAll('input[name="filter-element-type"]').forEach(cb => {
|
document.querySelectorAll('input[name="unified-type-filter"]').forEach(rb => {
|
||||||
cb.addEventListener('change', function () {
|
rb.addEventListener('change', function () {
|
||||||
applyFilters();
|
if (this.checked) currentTypeFilter = this.value;
|
||||||
updateFilterBadge();
|
|
||||||
schedulePreferencesSave();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -946,45 +884,26 @@ $(document).ready(function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
// init from locked element type
|
// init from locked element type
|
||||||
if (PREVENTIVE_ELEMENT_TYPE === 'assets') {
|
if (PREVENTIVE_ELEMENT_TYPE === 'assets') currentTypeFilter = 'assets';
|
||||||
const cbA = document.getElementById('type-cb-assets');
|
else if (PREVENTIVE_ELEMENT_TYPE === 'locations') currentTypeFilter = 'locations';
|
||||||
const cbL = document.getElementById('type-cb-locations');
|
|
||||||
const cbZ = document.getElementById('type-cb-management-zones');
|
|
||||||
if (cbA) cbA.checked = true;
|
|
||||||
if (cbL) cbL.checked = false;
|
|
||||||
if (cbZ) cbZ.checked = false;
|
|
||||||
} else if (PREVENTIVE_ELEMENT_TYPE === 'locations') {
|
|
||||||
const cbA = document.getElementById('type-cb-assets');
|
|
||||||
const cbL = document.getElementById('type-cb-locations');
|
|
||||||
const cbZ = document.getElementById('type-cb-management-zones');
|
|
||||||
if (cbA) cbA.checked = false;
|
|
||||||
if (cbL) cbL.checked = true;
|
|
||||||
if (cbZ) cbZ.checked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Add actions ─────────────────────────────────────────────────────
|
// ─── Add actions ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const addConfig = getUnifiedAddModalConfig();
|
const addConfig = getUnifiedAddModalConfig();
|
||||||
const assetThematics = Array.isArray(addConfig.asset_thematics) ? addConfig.asset_thematics : [];
|
const assetThematics = Array.isArray(addConfig.asset_thematics) ? addConfig.asset_thematics : [];
|
||||||
const locationThematics = Array.isArray(addConfig.location_thematics) ? addConfig.location_thematics : [];
|
const locationThematics = Array.isArray(addConfig.location_thematics) ? addConfig.location_thematics : [];
|
||||||
const managementZoneThematics = Array.isArray(addConfig.management_zone_thematics) ? addConfig.management_zone_thematics : [];
|
|
||||||
const addButton = document.getElementById('btn-add-asset');
|
const addButton = document.getElementById('btn-add-asset');
|
||||||
const assetThematicSelect = document.getElementById('unified-add-asset-thematic');
|
const assetThematicSelect = document.getElementById('unified-add-asset-thematic');
|
||||||
const locationThematicSelect = document.getElementById('unified-add-location-thematic');
|
const locationThematicSelect = document.getElementById('unified-add-location-thematic');
|
||||||
const managementZoneThematicSelect = document.getElementById('unified-add-management-zone-thematic');
|
|
||||||
const chooserEmpty = document.getElementById('unified-add-chooser-empty');
|
const chooserEmpty = document.getElementById('unified-add-chooser-empty');
|
||||||
|
|
||||||
fillSelectWithOptions(assetThematicSelect, assetThematics, assetThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
fillSelectWithOptions(assetThematicSelect, assetThematics, assetThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
||||||
fillSelectWithOptions(locationThematicSelect, locationThematics, locationThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
fillSelectWithOptions(locationThematicSelect, locationThematics, locationThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
||||||
fillSelectWithOptions(managementZoneThematicSelect, managementZoneThematics, managementZoneThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible'));
|
|
||||||
|
|
||||||
if (assetThematicSelect) assetThematicSelect.disabled = !assetThematics.length;
|
if (assetThematicSelect) assetThematicSelect.disabled = !assetThematics.length;
|
||||||
if (locationThematicSelect) locationThematicSelect.disabled = !locationThematics.length;
|
if (locationThematicSelect) locationThematicSelect.disabled = !locationThematics.length;
|
||||||
if (managementZoneThematicSelect) managementZoneThematicSelect.disabled = !managementZoneThematics.length;
|
if (chooserEmpty) chooserEmpty.classList.toggle('d-none', assetThematics.length > 0 || locationThematics.length > 0);
|
||||||
|
if (addButton && !assetThematics.length && !locationThematics.length) {
|
||||||
const hasAnyAddRights = assetThematics.length > 0 || locationThematics.length > 0 || managementZoneThematics.length > 0;
|
|
||||||
if (chooserEmpty) chooserEmpty.classList.toggle('d-none', hasAnyAddRights);
|
|
||||||
if (addButton && !hasAnyAddRights) {
|
|
||||||
addButton.disabled = true;
|
addButton.disabled = true;
|
||||||
addButton.title = gettext('Vous n\'avez pas de droits de création');
|
addButton.title = gettext('Vous n\'avez pas de droits de création');
|
||||||
}
|
}
|
||||||
|
|
@ -1043,17 +962,6 @@ $(document).ready(function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('unified-add-management-zone-open')?.addEventListener('click', function () {
|
|
||||||
const thematicCode = managementZoneThematicSelect ? managementZoneThematicSelect.value : '';
|
|
||||||
if (!thematicCode) {
|
|
||||||
setModalAlert('unified-add-chooser-alert', gettext('Sélectionnez une thématique pour créer une zone de gestion.'), 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setModalAlert('unified-add-chooser-alert', '', 'info');
|
|
||||||
const baseUrl = addConfig.management_zone_create_url || '/assets/management-zones/add/';
|
|
||||||
window.location.href = `${baseUrl}?thematic=${encodeURIComponent(thematicCode)}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('unifiedAddAssetCategory')?.addEventListener('change', function () {
|
document.getElementById('unifiedAddAssetCategory')?.addEventListener('change', function () {
|
||||||
const thematicCode = document.getElementById('unified-add-asset-thematic-code')?.value || '';
|
const thematicCode = document.getElementById('unified-add-asset-thematic-code')?.value || '';
|
||||||
loadUnifiedAssetModels(thematicCode, this.value);
|
loadUnifiedAssetModels(thematicCode, this.value);
|
||||||
|
|
@ -1267,12 +1175,8 @@ $(document).ready(function () {
|
||||||
activeModels.clear();
|
activeModels.clear();
|
||||||
modelFilterCollapsed = true;
|
modelFilterCollapsed = true;
|
||||||
activeConditionalFilters = {};
|
activeConditionalFilters = {};
|
||||||
const cbA = document.getElementById('type-cb-assets');
|
const allRadio = document.getElementById('type-all');
|
||||||
const cbL = document.getElementById('type-cb-locations');
|
if (allRadio) { allRadio.checked = true; currentTypeFilter = 'all'; }
|
||||||
const cbZ = document.getElementById('type-cb-management-zones');
|
|
||||||
if (cbA) cbA.checked = true;
|
|
||||||
if (cbL) cbL.checked = true;
|
|
||||||
if (cbZ) cbZ.checked = true;
|
|
||||||
document.querySelectorAll('[name="filter-status"]').forEach(cb => { cb.checked = false; });
|
document.querySelectorAll('[name="filter-status"]').forEach(cb => { cb.checked = false; });
|
||||||
document.querySelectorAll('[name="filter-validation-status"]').forEach(cb => { cb.checked = false; });
|
document.querySelectorAll('[name="filter-validation-status"]').forEach(cb => { cb.checked = false; });
|
||||||
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
||||||
|
|
@ -1611,11 +1515,6 @@ function buildAssetsGeojsonPostBody(thematicCode, bbox, includeNonGeoFilters) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (thematicCode === 'nature') {
|
if (thematicCode === 'nature') {
|
||||||
if (Array.isArray(thematicFilters.management_type) && thematicFilters.management_type.length) {
|
|
||||||
payload.management_type = thematicFilters.management_type;
|
|
||||||
} else if (thematicFilters.management_type) {
|
|
||||||
payload.management_type = [thematicFilters.management_type];
|
|
||||||
}
|
|
||||||
if (thematicFilters.location_type) {
|
if (thematicFilters.location_type) {
|
||||||
payload.location_type = thematicFilters.location_type;
|
payload.location_type = thematicFilters.location_type;
|
||||||
}
|
}
|
||||||
|
|
@ -1961,13 +1860,11 @@ function findUnifiedRowIndex(rowRef) {
|
||||||
const rows = $table.bootstrapTable('getData') || [];
|
const rows = $table.bootstrapTable('getData') || [];
|
||||||
|
|
||||||
const isLocation = !!rowRef.is_location;
|
const isLocation = !!rowRef.is_location;
|
||||||
const isManagementZone = !!rowRef.is_management_zone;
|
|
||||||
const matches = (row) => {
|
const matches = (row) => {
|
||||||
if (!!row.is_location !== isLocation) return false;
|
if (!!row.is_location !== isLocation) return false;
|
||||||
if (!!row.is_management_zone !== isManagementZone) return false;
|
|
||||||
if (String(row.id) !== String(rowRef.id)) return false;
|
if (String(row.id) !== String(rowRef.id)) return false;
|
||||||
|
|
||||||
if (!isLocation && !isManagementZone) {
|
if (!isLocation) {
|
||||||
if (String(row.thematic || '') !== String(rowRef.thematic || '')) return false;
|
if (String(row.thematic || '') !== String(rowRef.thematic || '')) return false;
|
||||||
if (String(row.asset_type || '') !== String(rowRef.asset_type || '')) return false;
|
if (String(row.asset_type || '') !== String(rowRef.asset_type || '')) return false;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1981,7 +1878,7 @@ function findUnifiedRowIndex(rowRef) {
|
||||||
if (idx !== -1) return idx;
|
if (idx !== -1) return idx;
|
||||||
|
|
||||||
// Fallback in case some rows do not expose all discriminators.
|
// Fallback in case some rows do not expose all discriminators.
|
||||||
idx = rows.findIndex(row => !!row.is_location === isLocation && !!row.is_management_zone === isManagementZone && String(row.id) === String(rowRef.id));
|
idx = rows.findIndex(row => !!row.is_location === isLocation && String(row.id) === String(rowRef.id));
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2050,41 +1947,6 @@ function collectClickedAssets(clickEvent) {
|
||||||
element_type: p.element_type || '',
|
element_type: p.element_type || '',
|
||||||
detail_url: p.detail_url || null,
|
detail_url: p.detail_url || null,
|
||||||
feature_id: id,
|
feature_id: id,
|
||||||
extra_info: p.plots_count != null ? `${p.plots_count} plot(s)` : (p.bollard_type || ''),
|
|
||||||
management_type: p.management_type || '',
|
|
||||||
managing_authority: p.managing_authority || '',
|
|
||||||
is_remarkable: p.is_remarkable || false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(unique.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectClickedManagementZones(clickEvent) {
|
|
||||||
const layers = ['management-zones-fill', 'management-zones-line'].filter(layerId => map.getLayer(layerId));
|
|
||||||
if (!layers.length) return [];
|
|
||||||
|
|
||||||
const rendered = map.queryRenderedFeatures(clickEvent.point, { layers });
|
|
||||||
const unique = new Map();
|
|
||||||
|
|
||||||
rendered.forEach(f => {
|
|
||||||
const p = f.properties || {};
|
|
||||||
const id = f.id ?? p.id;
|
|
||||||
if (id == null) return;
|
|
||||||
const key = `zone|${p.thematic || ''}|${id}`;
|
|
||||||
if (unique.has(key)) return;
|
|
||||||
unique.set(key, {
|
|
||||||
id: id,
|
|
||||||
code: p.code || '',
|
|
||||||
name: p.name || '',
|
|
||||||
thematic: p.thematic || '',
|
|
||||||
thematic_name: f._thematic_name || p.thematic_name || p.thematic || '',
|
|
||||||
element_type: p.element_type || gettext('Zone de gestion'),
|
|
||||||
category: p.category || p.zone_type_name || '',
|
|
||||||
detail_url: p.detail_url || null,
|
|
||||||
color: p.color || '',
|
|
||||||
feature_id: id,
|
|
||||||
is_management_zone: true,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2154,29 +2016,12 @@ function bindAssetPopupActions(popupNode) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
popupNode.querySelectorAll('.zone-popup-focus-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const rowRef = {
|
|
||||||
id: this.dataset.zoneId,
|
|
||||||
thematic: this.dataset.thematic,
|
|
||||||
is_management_zone: true,
|
|
||||||
};
|
|
||||||
focusRowInUnifiedTable(rowRef);
|
|
||||||
|
|
||||||
if (typeof highlightMapFeature === 'function') {
|
|
||||||
highlightMapFeature(map, 'management_zones', rowRef.id, managementZonesHighlightState);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAssetClickPopup(clickEvent) {
|
function showAssetClickPopup(clickEvent) {
|
||||||
const assets = collectClickedAssets(clickEvent);
|
const assets = collectClickedAssets(clickEvent);
|
||||||
const locations = collectClickedLocations(clickEvent);
|
const locations = collectClickedLocations(clickEvent);
|
||||||
const managementZones = collectClickedManagementZones(clickEvent);
|
if (!assets.length && !locations.length) return;
|
||||||
if (!assets.length && !locations.length && !managementZones.length) return;
|
|
||||||
|
|
||||||
const maxItems = 8;
|
const maxItems = 8;
|
||||||
const visibleAssets = assets.slice(0, maxItems);
|
const visibleAssets = assets.slice(0, maxItems);
|
||||||
|
|
@ -2187,26 +2032,15 @@ function showAssetClickPopup(clickEvent) {
|
||||||
const type = escapeHtml(a.element_type || a.asset_type || '');
|
const type = escapeHtml(a.element_type || a.asset_type || '');
|
||||||
const thematic = escapeHtml(a.thematic_name || a.thematic || '');
|
const thematic = escapeHtml(a.thematic_name || a.thematic || '');
|
||||||
const name = escapeHtml(a.name || '');
|
const name = escapeHtml(a.name || '');
|
||||||
const extra = a.extra_info ? ` • <span class="badge bg-light text-primary border" style="font-size:0.7rem;">${escapeHtml(a.extra_info)}</span>` : '';
|
|
||||||
const detailAnchor = a.detail_url
|
const detailAnchor = a.detail_url
|
||||||
? `<a href="${escapeHtml(a.detail_url)}">${code}</a>`
|
? `<a href="${escapeHtml(a.detail_url)}">${code}</a>`
|
||||||
: `<span>${code}</span>`;
|
: `<span>${code}</span>`;
|
||||||
|
|
||||||
let iconPrefix = '';
|
|
||||||
if (typeof AssetSymbology !== 'undefined') {
|
|
||||||
const symb = AssetSymbology.getAssetSymbology(a);
|
|
||||||
const colorStyle = symb.color ? `color:${symb.color};` : '';
|
|
||||||
iconPrefix = AssetSymbology.renderIconHtml(symb, {
|
|
||||||
style: `${colorStyle} font-size: 1rem; vertical-align: -1px; margin-right: 5px;`,
|
|
||||||
imgClass: 'me-1'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="asset-popup-item" style="display:flex; gap:0.5rem; align-items:flex-start; justify-content:space-between; padding:0.25rem 0; border-bottom:1px solid #f1f3f5;">
|
<div class="asset-popup-item" style="display:flex; gap:0.5rem; align-items:flex-start; justify-content:space-between; padding:0.25rem 0; border-bottom:1px solid #f1f3f5;">
|
||||||
<div style="min-width:0;">
|
<div style="min-width:0;">
|
||||||
<div style="font-weight:600; line-height:1.1;">${iconPrefix}${detailAnchor}</div>
|
<div style="font-weight:600; line-height:1.1;">${detailAnchor}</div>
|
||||||
<div style="font-size:0.75rem; color:#6c757d; line-height:1.2;">${type}${thematic ? ' - ' + thematic : ''}${extra}</div>
|
<div style="font-size:0.75rem; color:#6c757d; line-height:1.2;">${type}${thematic ? ' - ' + thematic : ''}</div>
|
||||||
${name ? `<div style="font-size:0.75rem; color:#495057; line-height:1.2;">${name}</div>` : ''}
|
${name ? `<div style="font-size:0.75rem; color:#495057; line-height:1.2;">${name}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
|
@ -2279,56 +2113,10 @@ function showAssetClickPopup(clickEvent) {
|
||||||
`
|
`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const visibleZones = managementZones.slice(0, maxItems);
|
|
||||||
const hiddenZonesCount = Math.max(0, managementZones.length - visibleZones.length);
|
|
||||||
|
|
||||||
const zoneItemsHtml = visibleZones.map(z => {
|
|
||||||
const code = escapeHtml(z.code || '-');
|
|
||||||
const type = escapeHtml(z.element_type || gettext('Zone de gestion'));
|
|
||||||
const thematic = escapeHtml(z.thematic_name || z.thematic || '');
|
|
||||||
const cat = z.category ? ` • ${escapeHtml(z.category)}` : '';
|
|
||||||
const dotHtml = z.color ? `<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background-color:${escapeHtml(z.color)}; margin-right:4px; vertical-align:middle; box-shadow:0 0 0 1px rgba(0,0,0,0.15);"></span>` : '';
|
|
||||||
const name = escapeHtml(z.name || '');
|
|
||||||
const detailAnchor = z.detail_url
|
|
||||||
? `<a href="${escapeHtml(z.detail_url)}">${code}</a>`
|
|
||||||
: `<span>${code}</span>`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="zone-popup-item" style="display:flex; gap:0.5rem; align-items:flex-start; justify-content:space-between; padding:0.25rem 0; border-bottom:1px solid #f1f3f5;">
|
|
||||||
<div style="min-width:0;">
|
|
||||||
<div style="font-weight:600; line-height:1.1;">${detailAnchor}</div>
|
|
||||||
<div style="font-size:0.75rem; color:#6c757d; line-height:1.2;">${dotHtml}${type}${cat}${thematic ? ' - ' + thematic : ''}</div>
|
|
||||||
${name ? `<div style="font-size:0.75rem; color:#495057; line-height:1.2;">${name}</div>` : ''}
|
|
||||||
</div>
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-sm btn-light zone-popup-focus-btn"
|
|
||||||
data-zone-id="${escapeHtml(z.id)}"
|
|
||||||
data-thematic="${escapeHtml(z.thematic)}"
|
|
||||||
title="${escapeHtml(gettext('Voir dans le tableau'))}"
|
|
||||||
style="transform:translateX(-5px);">
|
|
||||||
<i class="bi bi-table"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const moreZonesHtml = hiddenZonesCount > 0
|
|
||||||
? `<div style="margin-top:0.35rem; font-size:0.75rem; color:#6c757d;">+${hiddenZonesCount} ${escapeHtml(gettext('autre(s) zone(s)'))}</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const managementZonesSectionHtml = managementZones.length > 0
|
|
||||||
? `
|
|
||||||
<div style="font-weight:700; margin:${(assets.length || locations.length) ? '0.6rem' : '0'} 0 0.35rem 0;">${escapeHtml(gettext('Zones de gestion trouvées'))}: ${managementZones.length}</div>
|
|
||||||
<div>${zoneItemsHtml}</div>
|
|
||||||
${moreZonesHtml}
|
|
||||||
`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const popupHtml = `
|
const popupHtml = `
|
||||||
<div style="min-width:260px; max-width:320px;">
|
<div style="min-width:260px; max-width:320px;">
|
||||||
${assetsSectionHtml}
|
${assetsSectionHtml}
|
||||||
${locationsSectionHtml}
|
${locationsSectionHtml}
|
||||||
${managementZonesSectionHtml}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -2519,23 +2307,6 @@ function refreshData() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Management zones: loaded per active thematic
|
|
||||||
if (cfg.management_zones_geojson_url) {
|
|
||||||
if (!('management_zones' in geojsonCache[code])) {
|
|
||||||
const url = cfg.management_zones_geojson_url + '?thematic=' + encodeURIComponent(code);
|
|
||||||
fetches.push(
|
|
||||||
fetchGeojsonFeatures(url, { signal }, signal)
|
|
||||||
.then(data => {
|
|
||||||
geojsonCache[code].management_zones = data.features || [];
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.name === 'AbortError') throw err;
|
|
||||||
geojsonCache[code].management_zones = [];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Assets: conditional loading
|
// ── Assets: conditional loading
|
||||||
const alwaysLoad = ALWAYS_LOAD_ASSETS_THEMATICS.has(code);
|
const alwaysLoad = ALWAYS_LOAD_ASSETS_THEMATICS.has(code);
|
||||||
const prevMode = assetsLoadModes[code] || null;
|
const prevMode = assetsLoadModes[code] || null;
|
||||||
|
|
@ -2611,10 +2382,6 @@ function refreshData() {
|
||||||
f._thematic_name = thematicName;
|
f._thematic_name = thematicName;
|
||||||
allFeatures.push(f);
|
allFeatures.push(f);
|
||||||
});
|
});
|
||||||
(cache.management_zones || []).forEach(f => {
|
|
||||||
f._thematic_name = thematicName;
|
|
||||||
allFeatures.push(f);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
renderModelFilters();
|
renderModelFilters();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
|
|
@ -2639,27 +2406,19 @@ function applyFilters() {
|
||||||
let features = allFeatures.slice();
|
let features = allFeatures.slice();
|
||||||
|
|
||||||
// Type filter
|
// Type filter
|
||||||
const cbAssets = document.getElementById('type-cb-assets');
|
if (currentTypeFilter === 'locations') {
|
||||||
const cbLocations = document.getElementById('type-cb-locations');
|
features = features.filter(f => f.properties && f.properties.is_location);
|
||||||
const cbZones = document.getElementById('type-cb-management-zones');
|
} else if (currentTypeFilter === 'assets') {
|
||||||
const showAssets = cbAssets ? cbAssets.checked : true;
|
features = features.filter(f => f.properties && !f.properties.is_location);
|
||||||
const showLocations = cbLocations ? cbLocations.checked : true;
|
}
|
||||||
const showManagementZones = cbZones ? cbZones.checked : true;
|
|
||||||
|
|
||||||
features = features.filter(f => {
|
|
||||||
if (!f.properties) return false;
|
|
||||||
if (f.properties.is_management_zone) return showManagementZones;
|
|
||||||
if (f.properties.is_location) return showLocations;
|
|
||||||
return showAssets;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Include no geom filter
|
// Include no geom filter
|
||||||
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
||||||
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
|
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
|
||||||
if (!includeNoGeom) {
|
if (!includeNoGeom) {
|
||||||
features = features.filter(f => {
|
features = features.filter(f => {
|
||||||
// Locations and management zones are never filtered by geometry checkbox (only assets)
|
// Locations are never filtered by geometry checkbox (only assets)
|
||||||
if (f.properties && (f.properties.is_location || f.properties.is_management_zone)) return true;
|
if (f.properties && f.properties.is_location) return true;
|
||||||
return f.geometry && (f.geometry.coordinates || f.geometry.type);
|
return f.geometry && (f.geometry.coordinates || f.geometry.type);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2676,7 +2435,7 @@ function applyFilters() {
|
||||||
const selectedValidationStatuses = Array.from(document.querySelectorAll('[name="filter-validation-status"]:checked')).map(cb => cb.value);
|
const selectedValidationStatuses = Array.from(document.querySelectorAll('[name="filter-validation-status"]:checked')).map(cb => cb.value);
|
||||||
if (selectedValidationStatuses.length) {
|
if (selectedValidationStatuses.length) {
|
||||||
features = features.filter(f => {
|
features = features.filter(f => {
|
||||||
if (f.properties && (f.properties.is_location || f.properties.is_management_zone)) return true;
|
if (f.properties && f.properties.is_location) return true;
|
||||||
return selectedValidationStatuses.includes(f.properties && f.properties.validation_status);
|
return selectedValidationStatuses.includes(f.properties && f.properties.validation_status);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2684,7 +2443,7 @@ function applyFilters() {
|
||||||
// Category filter
|
// Category filter
|
||||||
if (activeCategories.size) {
|
if (activeCategories.size) {
|
||||||
features = features.filter(f => {
|
features = features.filter(f => {
|
||||||
if (f.properties && (f.properties.is_location || f.properties.is_management_zone)) return true;
|
if (f.properties && f.properties.is_location) return true;
|
||||||
return activeCategories.has(getFeatureCategoryValue(f));
|
return activeCategories.has(getFeatureCategoryValue(f));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2692,7 +2451,7 @@ function applyFilters() {
|
||||||
// Model filter
|
// Model filter
|
||||||
if (activeModels.size) {
|
if (activeModels.size) {
|
||||||
features = features.filter(f => {
|
features = features.filter(f => {
|
||||||
if (f.properties && (f.properties.is_location || f.properties.is_management_zone)) return true;
|
if (f.properties && f.properties.is_location) return true;
|
||||||
return activeModels.has(getFeatureModelValue(f));
|
return activeModels.has(getFeatureModelValue(f));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2755,19 +2514,11 @@ function loadTableData(features) {
|
||||||
model: p.model || '',
|
model: p.model || '',
|
||||||
status: p.status || '',
|
status: p.status || '',
|
||||||
detail_url: p.detail_url || null,
|
detail_url: p.detail_url || null,
|
||||||
management_type: p.management_type || '',
|
|
||||||
managing_authority: p.managing_authority || '',
|
|
||||||
is_remarkable: p.is_remarkable || false,
|
|
||||||
scientific_name: p.scientific_name || '',
|
|
||||||
vernacular_name: p.vernacular_name || '',
|
|
||||||
is_dead: p.is_dead || false,
|
|
||||||
vitality: p.vitality || '',
|
|
||||||
lon: p.lon,
|
lon: p.lon,
|
||||||
lat: p.lat,
|
lat: p.lat,
|
||||||
length: Number.isFinite(length) ? length : 0,
|
length: Number.isFinite(length) ? length : 0,
|
||||||
area: Number.isFinite(area) ? area : 0,
|
area: Number.isFinite(area) ? area : 0,
|
||||||
is_location: p.is_location,
|
is_location: p.is_location,
|
||||||
is_management_zone: p.is_management_zone || false,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2788,7 +2539,6 @@ function loadTableData(features) {
|
||||||
|
|
||||||
var locationshighlightState = { previousId: null };
|
var locationshighlightState = { previousId: null };
|
||||||
var assetshighlightState = { previousId: null };
|
var assetshighlightState = { previousId: null };
|
||||||
var managementZonesHighlightState = { previousId: null };
|
|
||||||
|
|
||||||
|
|
||||||
function isPanelTextSpriteId(missingId) {
|
function isPanelTextSpriteId(missingId) {
|
||||||
|
|
@ -2861,20 +2611,15 @@ function rebuildMapSources() {
|
||||||
if (typeof map === 'undefined') return;
|
if (typeof map === 'undefined') return;
|
||||||
const locSrc = map.getSource('locations');
|
const locSrc = map.getSource('locations');
|
||||||
const asstSrc = map.getSource('assets');
|
const asstSrc = map.getSource('assets');
|
||||||
const zoneSrc = map.getSource('management_zones');
|
if (!locSrc || !asstSrc) return; // sources not yet added (before mapLoaded)
|
||||||
if (!locSrc || !asstSrc || !zoneSrc) return; // sources not yet added (before mapLoaded)
|
|
||||||
|
|
||||||
mergedLocationsGeojson = {
|
mergedLocationsGeojson = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && f.properties.is_location),
|
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && f.properties.is_location),
|
||||||
};
|
};
|
||||||
mergedManagementZonesGeojson = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && f.properties.is_management_zone),
|
|
||||||
};
|
|
||||||
mergedAssetsGeojson = {
|
mergedAssetsGeojson = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && !f.properties.is_location && !f.properties.is_management_zone),
|
features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && !f.properties.is_location),
|
||||||
};
|
};
|
||||||
|
|
||||||
applyUnifiedSignPanelOffsets(mergedAssetsGeojson);
|
applyUnifiedSignPanelOffsets(mergedAssetsGeojson);
|
||||||
|
|
@ -2882,7 +2627,6 @@ function rebuildMapSources() {
|
||||||
|
|
||||||
locSrc.setData(mergedLocationsGeojson);
|
locSrc.setData(mergedLocationsGeojson);
|
||||||
asstSrc.setData(mergedAssetsGeojson);
|
asstSrc.setData(mergedAssetsGeojson);
|
||||||
if (zoneSrc) zoneSrc.setData(mergedManagementZonesGeojson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2898,7 +2642,6 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
data: window.TERRITORY_MASK_URL || '/common/territory-mask-geojson/'
|
data: window.TERRITORY_MASK_URL || '/common/territory-mask-geojson/'
|
||||||
});
|
});
|
||||||
map.addSource('locations', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
map.addSource('locations', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||||
map.addSource('management_zones', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
|
||||||
map.addSource('assets', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
map.addSource('assets', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||||
|
|
||||||
// Add layers in render order: fills → lines → circles/symbols (see UNIFIED_LAYERS)
|
// Add layers in render order: fills → lines → circles/symbols (see UNIFIED_LAYERS)
|
||||||
|
|
@ -2949,12 +2692,7 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
|
|
||||||
// Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost)
|
// Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost)
|
||||||
if (typeof LayerSwitcherControl !== 'undefined') {
|
if (typeof LayerSwitcherControl !== 'undefined') {
|
||||||
const initialBasemap = window._restoredBasemap || localStorage.getItem('loko_active_basemap') || 'default';
|
|
||||||
const layerSwitcher = new LayerSwitcherControl({
|
const layerSwitcher = new LayerSwitcherControl({
|
||||||
initialBasemapId: initialBasemap,
|
|
||||||
onBasemapChange: function (basemapId) {
|
|
||||||
schedulePreferencesSave();
|
|
||||||
},
|
|
||||||
layers: [{
|
layers: [{
|
||||||
id: 'basemap',
|
id: 'basemap',
|
||||||
name: 'Fonds de plan & Orthophotos',
|
name: 'Fonds de plan & Orthophotos',
|
||||||
|
|
@ -2962,10 +2700,7 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
beforeLayer: (map && map.getLayer('territory-mask') ? 'territory-mask' : 'locations'),
|
beforeLayer: (map && map.getLayer('territory-mask') ? 'territory-mask' : 'locations'),
|
||||||
basemaps: window.MAP_BACKGROUNDS || [],
|
basemaps: window.MAP_BACKGROUNDS || [],
|
||||||
}],
|
}],
|
||||||
});
|
});// Reset view — fly back to initial Brussels Region view
|
||||||
window.unifiedLayerSwitcher = layerSwitcher;
|
|
||||||
|
|
||||||
// Reset view — fly back to initial Brussels Region view
|
|
||||||
map.addControl({
|
map.addControl({
|
||||||
onAdd(m) {
|
onAdd(m) {
|
||||||
this._map = m;
|
this._map = m;
|
||||||
|
|
@ -3047,10 +2782,10 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click interaction on map elements: show popup when assets are found at click position.
|
// Click interaction on map elements: show popup when assets are found at click position.
|
||||||
['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline', 'management-zones-fill', 'management-zones-line'].forEach(layerId => {
|
['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline'].forEach(layerId => {
|
||||||
if (!map.getLayer(layerId)) return;
|
if (!map.getLayer(layerId)) return;
|
||||||
map.on('click', layerId, showAssetClickPopup);
|
map.on('click', layerId, showAssetClickPopup);
|
||||||
if (['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'management-zones-fill'].includes(layerId)) {
|
if (['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical'].includes(layerId)) {
|
||||||
map.on('mouseenter', layerId, function () { map.getCanvas().style.cursor = 'pointer'; });
|
map.on('mouseenter', layerId, function () { map.getCanvas().style.cursor = 'pointer'; });
|
||||||
map.on('mouseleave', layerId, function () { map.getCanvas().style.cursor = ''; });
|
map.on('mouseleave', layerId, function () { map.getCanvas().style.cursor = ''; });
|
||||||
}
|
}
|
||||||
|
|
@ -3060,7 +2795,7 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
map.on('click', function (e) {
|
map.on('click', function (e) {
|
||||||
if (!assetClickPopup || !assetClickPopup.isOpen()) return;
|
if (!assetClickPopup || !assetClickPopup.isOpen()) return;
|
||||||
|
|
||||||
const interactiveLayers = ['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline', 'management-zones-fill', 'management-zones-line']
|
const interactiveLayers = ['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline']
|
||||||
.filter(layerId => map.getLayer(layerId));
|
.filter(layerId => map.getLayer(layerId));
|
||||||
if (!interactiveLayers.length) {
|
if (!interactiveLayers.length) {
|
||||||
assetClickPopup.remove();
|
assetClickPopup.remove();
|
||||||
|
|
@ -3121,7 +2856,7 @@ function updateFilterBadge() {
|
||||||
}, 0);
|
}, 0);
|
||||||
n += thematicBlocksCount;
|
n += thematicBlocksCount;
|
||||||
|
|
||||||
if (getActiveTypeFilters().length < 3) n += 1;
|
if (currentTypeFilter !== 'all') n += 1;
|
||||||
if (document.querySelectorAll('[name="filter-status"]:checked').length > 0) n += 1;
|
if (document.querySelectorAll('[name="filter-status"]:checked').length > 0) n += 1;
|
||||||
if (document.querySelectorAll('[name="filter-validation-status"]:checked').length > 0) n += 1;
|
if (document.querySelectorAll('[name="filter-validation-status"]:checked').length > 0) n += 1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,7 @@ function loadUnifiedMapIcons(map) {
|
||||||
// ── File-based icons (full colour, non-SDF) ─────────────────────────────
|
// ── File-based icons (full colour, non-SDF) ─────────────────────────────
|
||||||
// UNIFIED_ICON_URLS is set in the inline <script> of unified_index.html
|
// UNIFIED_ICON_URLS is set in the inline <script> of unified_index.html
|
||||||
// using {% static %} so the URL is resolved server-side.
|
// using {% static %} so the URL is resolved server-side.
|
||||||
const injected = (typeof UNIFIED_ICON_URLS !== 'undefined') ? UNIFIED_ICON_URLS : {};
|
const iconUrls = (typeof UNIFIED_ICON_URLS !== 'undefined') ? UNIFIED_ICON_URLS : {};
|
||||||
const iconUrls = (typeof AssetSymbology !== 'undefined' && AssetSymbology.getIconUrls)
|
|
||||||
? Object.assign({}, AssetSymbology.getIconUrls(), injected)
|
|
||||||
: injected;
|
|
||||||
|
|
||||||
// trafficlights: traffic_light_icon.svg (56 px)
|
// trafficlights: traffic_light_icon.svg (56 px)
|
||||||
if (iconUrls.trafficlights && !map.hasImage('icon-trafficlights')) {
|
if (iconUrls.trafficlights && !map.hasImage('icon-trafficlights')) {
|
||||||
|
|
@ -143,27 +140,6 @@ function loadUnifiedMapIcons(map) {
|
||||||
.then(data => { if (!map.hasImage('icon-nature')) map.addImage('icon-nature', data); })
|
.then(data => { if (!map.hasImage('icon-nature')) map.addImage('icon-nature', data); })
|
||||||
.catch(e => console.warn('unified_map_layers: could not load nature icon', e));
|
.catch(e => console.warn('unified_map_layers: could not load nature icon', e));
|
||||||
}
|
}
|
||||||
if (iconUrls.natureregional && !map.hasImage('icon-nature-regional')) {
|
|
||||||
loadNormalisedIcon(iconUrls.natureregional, 56)
|
|
||||||
.then(data => { if (!map.hasImage('icon-nature-regional')) map.addImage('icon-nature-regional', data); })
|
|
||||||
.catch(e => console.warn('unified_map_layers: could not load nature regional icon', e));
|
|
||||||
}
|
|
||||||
if (iconUrls.natureremarkable && !map.hasImage('icon-nature-remarkable')) {
|
|
||||||
loadNormalisedIcon(iconUrls.natureremarkable, 56)
|
|
||||||
.then(data => { if (!map.hasImage('icon-nature-remarkable')) map.addImage('icon-nature-remarkable', data); })
|
|
||||||
.catch(e => console.warn('unified_map_layers: could not load nature remarkable icon', e));
|
|
||||||
}
|
|
||||||
if (iconUrls.naturedead && !map.hasImage('icon-nature-dead')) {
|
|
||||||
loadNormalisedIcon(iconUrls.naturedead, 56)
|
|
||||||
.then(data => { if (!map.hasImage('icon-nature-dead')) map.addImage('icon-nature-dead', data); })
|
|
||||||
.catch(e => console.warn('unified_map_layers: could not load nature dead icon', e));
|
|
||||||
}
|
|
||||||
// naturegreensurface: dedicated green surface / lawn SVG (52 px)
|
|
||||||
if (iconUrls.naturegreensurface && !map.hasImage('icon-naturegreensurface')) {
|
|
||||||
loadNormalisedIcon(iconUrls.naturegreensurface, 52)
|
|
||||||
.then(data => { if (!map.hasImage('icon-naturegreensurface')) map.addImage('icon-naturegreensurface', data); })
|
|
||||||
.catch(e => console.warn('unified_map_layers: could not load naturegreensurface icon', e));
|
|
||||||
}
|
|
||||||
|
|
||||||
// structures: dedicated bridge/tunnel SVG (52 px)
|
// structures: dedicated bridge/tunnel SVG (52 px)
|
||||||
if (iconUrls.structures && !map.hasImage('icon-structures')) {
|
if (iconUrls.structures && !map.hasImage('icon-structures')) {
|
||||||
|
|
@ -225,22 +201,11 @@ function loadUnifiedMapIcons(map) {
|
||||||
.then(data => { if (!map.hasImage('icon-artworkmonument')) map.addImage('icon-artworkmonument', data); })
|
.then(data => { if (!map.hasImage('icon-artworkmonument')) map.addImage('icon-artworkmonument', data); })
|
||||||
.catch(e => console.warn('unified_map_layers: could not load artworkmonument icon', e));
|
.catch(e => console.warn('unified_map_layers: could not load artworkmonument icon', e));
|
||||||
}
|
}
|
||||||
// itsbollardplot: dedicated automatic bollard / plot SVG (42 px)
|
|
||||||
if (iconUrls.itsbollardplot && !map.hasImage('icon-itsbollardplot')) {
|
|
||||||
loadNormalisedIcon(iconUrls.itsbollardplot, 42)
|
|
||||||
.then(data => { if (!map.hasImage('icon-itsbollardplot')) map.addImage('icon-itsbollardplot', data); })
|
|
||||||
.catch(e => console.warn('unified_map_layers: could not load itsbollardplot icon', e));
|
|
||||||
}
|
|
||||||
// If the SVG above is unavailable, publiclighting falls back to Bootstrap glyph.
|
// If the SVG above is unavailable, publiclighting falls back to Bootstrap glyph.
|
||||||
|
|
||||||
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
|
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
|
||||||
// codepoints and colors derived from AssetSymbology (single source of truth)
|
// codepoints and colors derived from AssetSymbology (single source of truth)
|
||||||
const FILE_ICON_NAMES = new Set([
|
const FILE_ICON_NAMES = new Set(Object.keys(iconUrls).map(k => 'icon-' + k));
|
||||||
...Object.keys(iconUrls).map(k => 'icon-' + k),
|
|
||||||
'icon-nature-regional',
|
|
||||||
'icon-nature-remarkable',
|
|
||||||
'icon-nature-dead',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const GLYPH_ICONS = (typeof AssetSymbology !== 'undefined')
|
const GLYPH_ICONS = (typeof AssetSymbology !== 'undefined')
|
||||||
? AssetSymbology.getGlyphIcons()
|
? AssetSymbology.getGlyphIcons()
|
||||||
|
|
@ -419,34 +384,6 @@ const _unified_locations_fill = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Layer 1b — management zones fill ─────────────────────────────────────────
|
|
||||||
const _unified_management_zones_fill = {
|
|
||||||
id: 'management-zones-fill',
|
|
||||||
type: 'fill',
|
|
||||||
source: 'management_zones',
|
|
||||||
filter: ['any',
|
|
||||||
['==', ['geometry-type'], 'Polygon'],
|
|
||||||
['==', ['geometry-type'], 'MultiPolygon']
|
|
||||||
],
|
|
||||||
paint: {
|
|
||||||
'fill-color': ['coalesce', ['get', 'color'], '#10b981'],
|
|
||||||
'fill-opacity': 0.25,
|
|
||||||
'fill-antialias': false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Layer 3b — management zones line ─────────────────────────────────────────
|
|
||||||
const _unified_management_zones_line = {
|
|
||||||
id: 'management-zones-line',
|
|
||||||
type: 'line',
|
|
||||||
source: 'management_zones',
|
|
||||||
paint: {
|
|
||||||
'line-color': ['coalesce', ['get', 'color'], '#10b981'],
|
|
||||||
'line-width': 2,
|
|
||||||
'line-dasharray': [3, 2],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Layer 2 — asset fill (polygon assets: road surfaces, nature surfaces) ────
|
// ─── Layer 2 — asset fill (polygon assets: road surfaces, nature surfaces) ────
|
||||||
const _unified_assets_fill = (typeof AssetSymbology !== 'undefined' && AssetSymbology.getAssetsFillLayer)
|
const _unified_assets_fill = (typeof AssetSymbology !== 'undefined' && AssetSymbology.getAssetsFillLayer)
|
||||||
? AssetSymbology.getAssetsFillLayer('assets')
|
? AssetSymbology.getAssetsFillLayer('assets')
|
||||||
|
|
@ -669,7 +606,6 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -705,7 +641,6 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -741,7 +676,6 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -777,7 +711,6 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -813,7 +746,6 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -849,7 +781,6 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -885,7 +816,6 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -918,7 +848,6 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'thematic'], 'publiclighting'], 'bottom',
|
['==', ['get', 'thematic'], 'publiclighting'], 'bottom',
|
||||||
['==', ['get', 'thematic'], 'trafficlights'], 'bottom',
|
['==', ['get', 'thematic'], 'trafficlights'], 'bottom',
|
||||||
['==', ['get', 'thematic'], 'nature'], 'bottom',
|
['==', ['get', 'thematic'], 'nature'], 'bottom',
|
||||||
['==', ['get', 'asset_type'], 'itsbollardplot'], 'bottom',
|
|
||||||
'center'
|
'center'
|
||||||
],
|
],
|
||||||
'icon-offset': [
|
'icon-offset': [
|
||||||
|
|
@ -955,8 +884,6 @@ const _unified_assets_symbol = {
|
||||||
['all', ['==', ['get', 'thematic'], 'roads'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
|
['all', ['==', ['get', 'thematic'], 'roads'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
|
||||||
['all', ['==', ['get', 'asset_type'], 'naturerwiasset'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
|
['all', ['==', ['get', 'asset_type'], 'naturerwiasset'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
|
||||||
['==', ['get', 'asset_type'], 'naturegreensurface'], 0,
|
['==', ['get', 'asset_type'], 'naturegreensurface'], 0,
|
||||||
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0,
|
|
||||||
['match', ['geometry-type'], ['LineString', 'MultiLineString'], true, false], 0,
|
|
||||||
['boolean', ['feature-state', 'highlighted'], false], 1,
|
['boolean', ['feature-state', 'highlighted'], false], 1,
|
||||||
0.85
|
0.85
|
||||||
],
|
],
|
||||||
|
|
@ -1129,20 +1056,14 @@ const _unified_assets_line = (typeof AssetSymbology !== 'undefined' && AssetSymb
|
||||||
'line-color': [
|
'line-color': [
|
||||||
'case',
|
'case',
|
||||||
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
|
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
|
||||||
['==', ['get', 'asset_type'], 'itsbollardsystem'], '#0288d1',
|
|
||||||
THEMATIC_COLOR_EXPR
|
THEMATIC_COLOR_EXPR
|
||||||
],
|
],
|
||||||
'line-width': [
|
'line-width': [
|
||||||
'case',
|
'case',
|
||||||
['boolean', ['feature-state', 'highlighted'], false], 6,
|
['boolean', ['feature-state', 'highlighted'], false], 5,
|
||||||
['==', ['get', 'asset_type'], 'itsbollardsystem'], 5,
|
|
||||||
3
|
3
|
||||||
],
|
],
|
||||||
'line-opacity': [
|
'line-opacity': 0.8,
|
||||||
'case',
|
|
||||||
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0.95,
|
|
||||||
0.8
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
minzoom: 8,
|
minzoom: 8,
|
||||||
};
|
};
|
||||||
|
|
@ -1224,12 +1145,10 @@ const UNIFIED_LAYERS = [
|
||||||
_unified_territory_mask, // 0. territory mask (outside commune)
|
_unified_territory_mask, // 0. territory mask (outside commune)
|
||||||
_unified_territory_outline, // 0b. territory boundary line
|
_unified_territory_outline, // 0b. territory boundary line
|
||||||
_unified_locations_fill, // 1. polygon fills (bottom)
|
_unified_locations_fill, // 1. polygon fills (bottom)
|
||||||
_unified_management_zones_fill, // 1b. management zones fill
|
|
||||||
_unified_assets_fill, // 2. polygon fills
|
_unified_assets_fill, // 2. polygon fills
|
||||||
_unified_locations_outline, // 3. lines
|
_unified_locations_outline, // 3. lines
|
||||||
_unified_management_zones_line, // 3b. management zones line
|
_unified_assets_line, // 3b. asset lines
|
||||||
_unified_assets_line, // 3c. asset lines
|
_unified_assets_polygon_outline, // 3c. polygon asset outlines
|
||||||
_unified_assets_polygon_outline, // 3d. polygon asset outlines
|
|
||||||
_unified_locations_circle, // 4. circles
|
_unified_locations_circle, // 4. circles
|
||||||
_unified_locations_radar_outline,
|
_unified_locations_radar_outline,
|
||||||
_unified_locations_radar,
|
_unified_locations_radar,
|
||||||
|
|
|
||||||
|
|
@ -364,9 +364,6 @@
|
||||||
<script src="{% url 'javascript-catalog' %}"></script>
|
<script src="{% url 'javascript-catalog' %}"></script>
|
||||||
<script src="{% static 'common/base_script.js' %}"></script>
|
<script src="{% static 'common/base_script.js' %}"></script>
|
||||||
<script src="{% static 'common/base_map.js' %}"></script>
|
<script src="{% static 'common/base_map.js' %}"></script>
|
||||||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
|
||||||
<script src="{% static 'contracts/js/contracts_map_overlay.js' %}"></script>
|
|
||||||
<script src="{% static 'assets/js/management_zones_map_overlay.js' %}"></script>
|
|
||||||
<script src="{% static 'assets/assets_index.js' %}"></script>
|
<script src="{% static 'assets/assets_index.js' %}"></script>
|
||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
{% endblock extra_script %}
|
{% endblock extra_script %}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,35 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<ul class="list-group list-group-flush bg-transparent border-0">
|
<ul class="list-group list-group-flush bg-transparent">
|
||||||
{% for asset_node in asset_nodes %}
|
{% for asset_node in asset_nodes %}
|
||||||
<li class="list-group-item bg-transparent border-0 py-1 px-0">
|
<li class="list-group-item bg-transparent border-0 py-1 px-0">
|
||||||
<!-- Row Header -->
|
<!-- Row Header -->
|
||||||
<div class="d-flex align-items-center justify-content-between py-1 px-3">
|
<div class="d-flex align-items-center justify-content-between py-1 px-2">
|
||||||
<!-- Contenu gauche (indenté selon la profondeur) -->
|
<!-- Contenu gauche (indenté selon la profondeur) -->
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap" style="padding-left: calc(({{ depth|default:1 }} - 1) * 1.25rem);">
|
<div class="d-flex align-items-center gap-2 flex-wrap" style="padding-left: calc(({{ depth|default:1 }} - 1) * 1.25rem);">
|
||||||
{% if asset_node.children %}
|
{% if asset_node.children %}
|
||||||
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn"
|
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn collapsed"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="collapse"
|
data-bs-toggle="collapse"
|
||||||
data-bs-target="#tree-asset-{{ asset_node.asset.id }}"
|
data-bs-target="#tree-asset-{{ asset_node.asset.id }}"
|
||||||
aria-expanded="true">
|
aria-expanded="false">
|
||||||
<i class="bi bi-chevron-down small"></i>
|
<i class="bi bi-chevron-right small"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="d-inline-block text-center text-muted" style="width: 1.35rem;"><i class="bi bi-dash small"></i></span>
|
<span class="px-1"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<i class="bi bi-gear-fill text-info small"></i>
|
||||||
|
|
||||||
<!-- Icône colorée selon la famille d'asset -->
|
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
|
||||||
<i class="bi {{ asset_node.asset.get_icon_class|default:'bi-tools' }}" style="font-size: 1rem; color: {{ asset_node.asset.theme_icon_color }};"></i>
|
<a href="{{ asset_node.asset.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
|
||||||
|
|
||||||
<!-- Code cliquable avec couleur thématique -->
|
|
||||||
<a href="{{ asset_node.asset.get_absolute_url }}" class="fw-bold text-decoration-underline small" style="color: {{ asset_node.asset.theme_color }};">
|
|
||||||
{{ asset_node.asset.code }}
|
{{ asset_node.asset.code }}
|
||||||
</a>
|
</a>
|
||||||
<span class="small text-dark">{{ asset_node.asset.get_name }}</span>
|
<span class="small">{{ asset_node.asset.get_name }}</span>
|
||||||
|
<span class="badge bg-info-subtle text-info border small ms-1">{{ asset_node.asset.get_asset_type_display }}</span>
|
||||||
<!-- Badge Macro-lot sobre -->
|
|
||||||
<span class="badge bg-light text-muted border small">{{ asset_node.asset.get_macro_lot }}</span>
|
|
||||||
|
|
||||||
<!-- Badge Typologie avec couleur thématique différenciée -->
|
|
||||||
<span class="badge {{ asset_node.asset.theme_badge_class }} small" style="{{ asset_node.asset.theme_badge_style }}">
|
|
||||||
{{ asset_node.asset.get_asset_type_display }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{% if asset_node.asset.status == 'inactive' %}
|
|
||||||
<span class="badge bg-danger-subtle text-danger border small">{% translate "Inactif" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if asset_node.children %}
|
{% if asset_node.children %}
|
||||||
<div id="tree-asset-{{ asset_node.asset.id }}" class="collapse show tree-collapse-node">
|
<div id="tree-asset-{{ asset_node.asset.id }}" class="collapse mt-1">
|
||||||
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=asset_node.children depth=depth|default:1|add:1 only %}
|
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=asset_node.children depth=depth|default:1|add:1 only %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -133,40 +133,6 @@
|
||||||
<th class="text-muted">{% translate "Surface Utile / Brut" %} :</th>
|
<th class="text-muted">{% translate "Surface Utile / Brut" %} :</th>
|
||||||
<td>{{ building.usable_surface_m2|default:"-" }} m² / {{ building.total_surface_m2|default:"-" }} m²</td>
|
<td>{{ building.usable_surface_m2|default:"-" }} m² / {{ building.total_surface_m2|default:"-" }} m²</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if building.room_number %}
|
|
||||||
<tr>
|
|
||||||
<th class="text-muted">{% translate "Numéro de local / porte" %} :</th>
|
|
||||||
<td><span class="badge bg-secondary-subtle text-secondary border font-monospace"><i class="bi bi-tag me-1"></i>{{ building.room_number }}</span></td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if building.surface_m2 %}
|
|
||||||
<tr>
|
|
||||||
<th class="text-muted">{% translate "Surface spécifique" %} :</th>
|
|
||||||
<td>{{ building.surface_m2 }} m²</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if building.ceiling_height_m %}
|
|
||||||
<tr>
|
|
||||||
<th class="text-muted">{% translate "Hauteur sous plafond" %} :</th>
|
|
||||||
<td>{{ building.ceiling_height_m }} m</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<th class="text-muted">{% translate "Chauffage du local" %} :</th>
|
|
||||||
<td>{% if building.is_heated %}<span class="text-success"><i class="bi bi-fire me-1"></i>{% translate "Chauffé" %}</span>{% else %}<span class="text-muted"><i class="bi bi-slash-circle me-1"></i>{% translate "Non chauffé" %}</span>{% endif %}</td>
|
|
||||||
</tr>
|
|
||||||
{% if building.floor_type %}
|
|
||||||
<tr>
|
|
||||||
<th class="text-muted">{% translate "Revêtement de sol" %} :</th>
|
|
||||||
<td>{{ building.get_floor_type_display }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if building.fire_compartment %}
|
|
||||||
<tr>
|
|
||||||
<th class="text-muted">{% translate "Compartiment coupe-feu" %} :</th>
|
|
||||||
<td>{{ building.fire_compartment }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-muted">{{ regional_terms.public_access_category }} :</th>
|
<th class="text-muted">{{ regional_terms.public_access_category }} :</th>
|
||||||
<td>{{ building.erp_category|default:"-" }}</td>
|
<td>{{ building.erp_category|default:"-" }}</td>
|
||||||
|
|
@ -180,21 +146,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card 3: Arborescence Unifiée des Localisations & Assets -->
|
<!-- Card 3: Arborescence Unifiée des Localisations & Assets -->
|
||||||
<div class="card shadow-sm mb-4" id="card-hierarchy-tree">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center flex-wrap gap-2">
|
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-diagram-3 me-2"></i>{% translate "Arborescence Hiérarchique" %}</span>
|
<span><i class="bi bi-diagram-3 me-2"></i>{% translate "Arborescence Hiérarchique" %}</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<div class="btn-group btn-group-sm" role="group" id="tree-expand-collapse-group">
|
|
||||||
<button type="button" class="btn btn-outline-secondary py-0 px-2 small" id="btn-tree-collapse-all" title="{% translate 'Tout replier' %}">
|
|
||||||
<i class="bi bi-arrows-collapse me-1"></i>{% translate "Tout replier" %}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary py-0 px-2 small" id="btn-tree-expand-all" title="{% translate 'Tout déplier' %}">
|
|
||||||
<i class="bi bi-arrows-expand me-1"></i>{% translate "Tout déplier" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span class="badge bg-primary-subtle text-primary border">{{ location_tree.children|length }} {% translate "sous-localisation(s)" %}</span>
|
<span class="badge bg-primary-subtle text-primary border">{{ location_tree.children|length }} {% translate "sous-localisation(s)" %}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
|
|
||||||
<!-- Localisation Parente (si existante) -->
|
<!-- Localisation Parente (si existante) -->
|
||||||
|
|
@ -221,198 +177,64 @@
|
||||||
<!-- Localisation Courante (Racine de l'affichage) -->
|
<!-- Localisation Courante (Racine de l'affichage) -->
|
||||||
<div class="p-2 border rounded bg-white mb-2">
|
<div class="p-2 border rounded bg-white mb-2">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn"
|
<i class="bi bi-building text-primary fs-5"></i>
|
||||||
type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#tree-root-content"
|
|
||||||
aria-expanded="true">
|
|
||||||
<i class="bi bi-chevron-down small"></i>
|
|
||||||
</button>
|
|
||||||
<i class="bi {{ building.get_icon_class }} text-primary fs-5"></i>
|
|
||||||
<span class="fw-bold fs-6 text-primary">{{ building.code }}</span>
|
<span class="fw-bold fs-6 text-primary">{{ building.code }}</span>
|
||||||
{% if building.room_number %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary border ms-1 font-monospace"><i class="bi bi-tag me-1"></i>{{ building.room_number }}</span>
|
|
||||||
{% endif %}
|
|
||||||
<span class="fw-bold fs-6">- {{ building.get_name }}</span>
|
<span class="fw-bold fs-6">- {{ building.get_name }}</span>
|
||||||
<span class="badge bg-primary text-white ms-1">{{ building.get_location_type_display }}</span>
|
<span class="badge bg-primary text-white ms-1">{{ building.get_location_type_display }}</span>
|
||||||
|
|
||||||
{% if location_tree.assets %}
|
{% if location_tree.assets %}
|
||||||
<span class="badge bg-info-subtle text-info border small ms-1" title="{% translate 'Assets rattachés directement au bâtiment' %}">
|
<button class="btn btn-sm btn-outline-info py-0 px-2 small ms-2 text-nowrap"
|
||||||
<i class="bi bi-tools me-1"></i>{{ location_tree.assets|length }} {% translate "asset(s)" %}
|
type="button"
|
||||||
</span>
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#tree-assets-root"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="bi bi-tools me-1"></i>Assets ({{ location_tree.assets|length }})
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tree-root-content" class="collapse show mt-2">
|
<!-- Collapsible Assets de la localisation courante (NON VISIBLES PAR DÉFAUT) -->
|
||||||
<!-- Assets rattachés directement au bâtiment racine -->
|
|
||||||
{% if location_tree.assets %}
|
{% if location_tree.assets %}
|
||||||
<div class="tree-assets-group">
|
<div id="tree-assets-root" class="collapse mt-2 ms-4">
|
||||||
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=location_tree.assets depth=2 only %}
|
<div class="card card-body bg-light border-info-subtle p-2 mb-2">
|
||||||
|
<div class="fw-bold small text-info mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ building.code }} :</div>
|
||||||
|
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=location_tree.assets only %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Sous-localisations & leurs assets -->
|
<!-- Sous-localisations & leurs assets -->
|
||||||
<div class="tree-locations-group">
|
<div class="mt-2">
|
||||||
{% include "assets/buildings/publicbuilding_location_tree.html" with node=location_tree depth=1 only %}
|
{% include "assets/buildings/publicbuilding_location_tree.html" with node=location_tree depth=1 only %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column: Localisation & Plans d'étage / Carte interactive -->
|
<!-- Right Column: Localisation & Documents (sous la carte) -->
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
|
|
||||||
<!-- Card Localisation, Carte interactive & Plans d'étage -->
|
<!-- Card Localisation & Carte interactive -->
|
||||||
<div class="card shadow-sm mb-4" id="card-location-plans">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center flex-wrap gap-2 py-2">
|
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex align-items-center">
|
<span><i class="bi bi-geo-alt me-2"></i>{% translate "Localisation & Carte" %}</span>
|
||||||
{% if level_specific_mode %}
|
|
||||||
<span class="fw-bold text-dark"><i class="bi bi-layers text-primary me-2"></i>{% translate "Plan du niveau" %} : <span class="text-primary">{{ active_floor.name|default:building.name }}</span></span>
|
|
||||||
{% else %}
|
|
||||||
<span class="fw-bold text-dark"><i class="bi bi-geo-alt text-primary me-2"></i>{% translate "Localisation & Plans" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<div class="btn-group btn-group-sm" role="group" id="view-mode-toggle">
|
|
||||||
<button type="button" class="btn btn-outline-primary active" id="btn-toggle-plans" title="{% translate "Afficher les plans d'étage" %}">
|
|
||||||
<i class="bi bi-layers me-1"></i>{% translate "Plans" %}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-primary" id="btn-toggle-map" title="{% translate "Afficher la carte du site" %}">
|
|
||||||
<i class="bi bi-map me-1"></i>{% translate "Carte" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% if center_lat and center_lon %}
|
{% if center_lat and center_lon %}
|
||||||
<a href="https://www.google.com/maps?cbll={{ center_lat_json }},{{ center_lon_json }}&cbp=12,0,0,0,5&layer=c&panoid="
|
<a href="https://www.google.com/maps?cbll={{ center_lat|floatformat:'6u'}},{{ center_lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid="
|
||||||
target="_blank" class="btn btn-sm btn-outline-secondary py-0 px-2" title="Street View">
|
target="_blank" class="btn btn-sm btn-outline-secondary py-0 px-2">
|
||||||
<img src="{% static 'common/google-street-view-logo.png' %}" alt="Street View" height="14" class="me-1">
|
<img src="{% static 'common/google-street-view-logo.png' %}" alt="Street View" height="14" class="me-1">
|
||||||
Street View
|
Street View
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 1. VUE PLANS D'ÉTAGE -->
|
|
||||||
<div id="section-plans" class="card-body p-0">
|
|
||||||
{% if not level_specific_mode and floors_data %}
|
|
||||||
<!-- Sélecteur d'étage horizontal -->
|
|
||||||
<div class="bg-light border-bottom px-3 py-2 d-flex align-items-center gap-1 overflow-auto" id="floors-nav-bar" style="white-space: nowrap;">
|
|
||||||
<span class="text-muted small fw-semibold me-2"><i class="bi bi-filter me-1"></i>{% translate "Niveaux" %} :</span>
|
|
||||||
{% for f in floors_data %}
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-sm floor-nav-btn {% if f.id == active_floor.id %}btn-primary text-white shadow-sm{% else %}btn-outline-secondary{% endif %}"
|
|
||||||
data-floor-id="{{ f.id }}"
|
|
||||||
data-plan-url="{{ f.plan_url }}"
|
|
||||||
data-has-plan="{{ f.has_plan|yesno:'true,false' }}"
|
|
||||||
data-floor-name="{{ f.name }}"
|
|
||||||
data-surface="{{ f.surface_m2|default:'' }}"
|
|
||||||
data-rooms-count="{{ f.rooms_count }}"
|
|
||||||
data-assets-count="{{ f.assets_count }}"
|
|
||||||
data-floor-url="{{ f.url }}">
|
|
||||||
{{ f.name }}
|
|
||||||
{% if f.has_plan %}
|
|
||||||
<span class="badge bg-white text-primary ms-1" title="{% translate "Plan architectural disponible" %}">📐</span>
|
|
||||||
{% endif %}
|
|
||||||
</button>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Conteneur du viewer interactif -->
|
|
||||||
<div id="plan-viewer-wrapper" style="position: relative; height: 380px; width: 100%; background: #0F172A05; overflow: hidden; display: flex; align-items: center; justify-content: center;">
|
|
||||||
|
|
||||||
<!-- Floating Controls (Zoom / Pan / Fullscreen / Download) -->
|
|
||||||
<div class="btn-group-vertical btn-group-sm shadow-sm" style="position: absolute; top: 12px; right: 12px; z-index: 10; background: rgba(255, 255, 255, 0.92); backdrop-filter: blur(4px); border-radius: 6px; border: 1px solid #CBD5E1;">
|
|
||||||
<button type="button" class="btn btn-light py-1 px-2" id="plan-zoom-in" title="{% translate "Zoom avant (+)" %}">
|
|
||||||
<i class="bi bi-zoom-in"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-light py-1 px-2" id="plan-zoom-out" title="{% translate "Zoom arrière (-)" %}">
|
|
||||||
<i class="bi bi-zoom-out"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-light py-1 px-2" id="plan-reset" title="{% translate "Recentrer / Ajuster (100%)" %}">
|
|
||||||
<i class="bi bi-arrows-angle-contract"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-light py-1 px-2" id="plan-fullscreen" title="{% translate "Plein écran" %}">
|
|
||||||
<i class="bi bi-arrows-fullscreen"></i>
|
|
||||||
</button>
|
|
||||||
<a href="{{ active_floor.plan_url|default:'#' }}" target="_blank" class="btn btn-light py-1 px-2" id="plan-download-link" title="{% translate "Ouvrir / Télécharger le plan" %}" {% if not active_floor.has_plan %}style="display:none;"{% endif %}>
|
|
||||||
<i class="bi bi-box-arrow-up-right"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Indicateur du niveau affiché -->
|
|
||||||
<div class="badge bg-dark bg-opacity-75 text-white px-2 py-1" style="position: absolute; bottom: 12px; left: 12px; z-index: 10; font-size: 0.8rem; pointer-events: none;" id="plan-level-indicator">
|
|
||||||
<i class="bi bi-layers me-1"></i><span id="plan-active-name">{{ active_floor.name|default:building.name }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pan/Zoom Canvas -->
|
|
||||||
<div id="plan-viewport" style="width: 100%; height: 100%; cursor: grab; display: flex; align-items: center; justify-content: center; transform-origin: center center; transition: transform 0.08s ease-out;">
|
|
||||||
<img id="plan-image"
|
|
||||||
src="{{ active_floor.plan_url|default:'' }}"
|
|
||||||
alt="{% translate "Plan d'étage" %}"
|
|
||||||
style="max-width: 95%; max-height: 95%; object-fit: contain; user-select: none; pointer-events: none; {% if not active_floor.has_plan %}display: none;{% endif %}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State when floor has no plan -->
|
|
||||||
<div id="plan-empty-state" class="text-center p-4" style="{% if active_floor.has_plan %}display: none;{% else %}display: block;{% endif %}">
|
|
||||||
<i class="bi bi-file-earmark-image fs-1 text-muted d-block mb-2"></i>
|
|
||||||
<h6 class="text-muted mb-1">{% translate "Aucun plan associé à ce niveau" %}</h6>
|
|
||||||
<p class="text-muted small mb-3">{% translate "Vous pouvez téléverser un plan d'étage (SVG, image ou PDF) pour ce niveau." %}</p>
|
|
||||||
{% if can_edit %}
|
|
||||||
<a href="{% url 'assets:publicbuilding_edit' building.pk %}" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="bi bi-upload me-1"></i>{% translate "Associer un plan" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer info du niveau actif (locaux & caractéristiques) -->
|
|
||||||
<div class="card-footer bg-white p-3 border-top" id="plan-floor-info">
|
|
||||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
|
|
||||||
<div>
|
|
||||||
<strong class="text-dark" id="info-floor-title">{{ active_floor.name|default:building.name }}</strong>
|
|
||||||
<span class="text-muted small ms-2" id="info-floor-code">({{ active_floor.code|default:building.code }})</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<span class="badge bg-light text-dark border" id="info-floor-surface">
|
|
||||||
{% if active_floor.surface_m2 %}
|
|
||||||
<i class="bi bi-bounding-box me-1 text-primary"></i>{{ active_floor.surface_m2|floatformat:1 }} m²
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
<span class="badge bg-light text-dark border" id="info-floor-rooms">
|
|
||||||
<i class="bi bi-door-closed me-1 text-primary"></i><span id="info-rooms-count">{{ active_floor.rooms_count|default:0 }}</span> {% translate "local/locaux" %}
|
|
||||||
</span>
|
|
||||||
<span class="badge bg-light text-dark border" id="info-floor-assets">
|
|
||||||
<i class="bi bi-tools me-1 text-primary"></i><span id="info-assets-count">{{ active_floor.assets_count|default:0 }}</span> {% translate "équipement(s)" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Chips des locaux / pièces situés sur cet étage -->
|
|
||||||
<div id="info-floor-rooms-chips" class="d-flex flex-wrap gap-1 mt-2">
|
|
||||||
{% for r in active_floor.rooms %}
|
|
||||||
<a href="{{ r.url }}" class="badge bg-light text-dark border text-decoration-none py-1 px-2" title="{{ r.location_type_display }}">
|
|
||||||
<i class="bi bi-door-open me-1 text-primary"></i><strong>{{ r.room_number|default:r.code }}</strong> - {{ r.name }}
|
|
||||||
</a>
|
|
||||||
{% empty %}
|
|
||||||
<span class="text-muted small fst-italic">{% translate "Aucun sous-local direct répertorié sur ce niveau." %}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 2. VUE CARTE DU SITE / EMPRISE (MapLibre) -->
|
|
||||||
<div id="section-map" style="display: none;">
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div id="building-map" style="height: 380px; width: 100%;"></div>
|
<div id="building-map" style="height: 340px; width: 100%; border-radius: 0 0 4px 4px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer bg-white p-3 border-top">
|
<div class="card-footer bg-white p-3">
|
||||||
<p class="mb-2 small">
|
<p class="mb-2 small">
|
||||||
<strong>{% translate "Adresse" %} :</strong> {{ building.address|default:_("Non renseignée") }}
|
<strong>{% translate "Adresse" %} :</strong> {{ building.address|default:_("Non renseignée") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -423,9 +245,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card Documents associés (placée sous les plans et la carte) -->
|
<!-- Card Documents associés (placée sous la localisation) -->
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<span class="fw-bold"><i class="bi bi-file-earmark-text me-2"></i>{% translate "Documents associés" %} ({{ documents.count }})</span>
|
<span class="fw-bold"><i class="bi bi-file-earmark-text me-2"></i>{% translate "Documents associés" %} ({{ documents.count }})</span>
|
||||||
|
|
@ -437,30 +258,15 @@
|
||||||
<div class="row row-cols-1 row-cols-sm-2 g-3">
|
<div class="row row-cols-1 row-cols-sm-2 g-3">
|
||||||
{% for doc in documents %}
|
{% for doc in documents %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card h-100 border p-2 text-center position-relative">
|
<div class="card h-100 border text-center p-2">
|
||||||
{% if doc.document_type == 'floor_plan' %}
|
{% if doc.document.file %}
|
||||||
<span class="badge bg-primary text-white position-absolute top-0 end-0 m-1" style="font-size: 0.65rem;">{% translate "Plan" %}</span>
|
|
||||||
<i class="bi bi-layers text-primary fs-2 mb-1"></i>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary text-white position-absolute top-0 end-0 m-1" style="font-size: 0.65rem;">{{ doc.get_document_type_display }}</span>
|
|
||||||
<i class="bi bi-file-earmark-pdf text-danger fs-2 mb-1"></i>
|
<i class="bi bi-file-earmark-pdf text-danger fs-2 mb-1"></i>
|
||||||
{% endif %}
|
<div class="small text-truncate w-100 fw-bold" title="{{ doc.document.file.name }}">{{ doc.document.file.name }}</div>
|
||||||
|
<a href="{{ doc.document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary mt-2">
|
||||||
<div class="small text-truncate w-100 fw-bold" title="{{ doc.description|default:doc.file.name }}">
|
|
||||||
{{ doc.description|default:doc.file.name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if doc.floor_location %}
|
|
||||||
<div class="small text-muted text-truncate mb-1" style="font-size: 0.75rem;">
|
|
||||||
<i class="bi bi-layers-half me-1"></i>{{ doc.floor_location.name }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if doc.file %}
|
|
||||||
<a href="{% url 'private_file_download' %}?app=assets&model=PublicBuildingDocument&pk={{ doc.pk }}&field=file" target="_blank" class="btn btn-sm btn-outline-secondary mt-auto py-0 px-2" style="font-size: 0.8rem;">
|
|
||||||
<i class="bi bi-download me-1"></i>{% translate "Télécharger" %}
|
<i class="bi bi-download me-1"></i>{% translate "Télécharger" %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<i class="bi bi-file-earmark text-secondary fs-2 mb-1"></i>
|
||||||
<span class="text-muted small">{% translate "Fichier indisponible" %}</span>
|
<span class="text-muted small">{% translate "Fichier indisponible" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -493,13 +299,11 @@
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% translate "Code / Nom" %}</th>
|
<th>{% translate "Code / Nom" %}</th>
|
||||||
<th>{% translate "Lot / Type d'asset" %}</th>
|
<th>{% translate "Type d'asset" %}</th>
|
||||||
<th>{% translate "Caractéristiques & Spécifications" %}</th>
|
|
||||||
<th>{% translate "Localisation spécifique" %}</th>
|
<th>{% translate "Localisation spécifique" %}</th>
|
||||||
<th>{% translate "Asset Parent" %}</th>
|
<th>{% translate "Asset Parent" %}</th>
|
||||||
<th>{% translate "Statut" %}</th>
|
<th>{% translate "Statut" %}</th>
|
||||||
<th>{% translate "Prochain contrôle" %}</th>
|
<th>{% translate "Prochain contrôle" %}</th>
|
||||||
<th>{% translate "Maintenance & Prestataire" %}</th>
|
|
||||||
<th class="text-end pe-3">{% translate "Actions" %}</th>
|
<th class="text-end pe-3">{% translate "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -507,39 +311,18 @@
|
||||||
{% for eq in equipments %}
|
{% for eq in equipments %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ eq.get_absolute_url }}" class="fw-bold text-decoration-none" style="color: {{ eq.theme_color }};">
|
<a href="{{ eq.get_absolute_url }}" class="fw-bold text-decoration-none">{{ eq.code }}</a>
|
||||||
<i class="bi {{ eq.get_icon_class }} me-1" style="color: {{ eq.theme_icon_color }};"></i>{{ eq.code }}
|
|
||||||
</a>
|
|
||||||
<div class="small text-muted">{{ eq.get_name }}</div>
|
<div class="small text-muted">{{ eq.get_name }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-light text-muted border me-1">{{ eq.get_macro_lot }}</span>
|
<span class="badge bg-info-subtle text-info border">
|
||||||
<span class="badge {{ eq.theme_badge_class }} d-block mt-1" style="{{ eq.theme_badge_style }}">
|
|
||||||
{{ eq.get_asset_type_display }}
|
{{ eq.get_asset_type_display }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
{% if eq.dimensions or eq.surface_m2 %}
|
|
||||||
<div class="small fw-semibold">{% if eq.dimensions %}{{ eq.dimensions }}{% endif %}{% if eq.surface_m2 %} ({{ eq.surface_m2 }} m²){% endif %}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if eq.power_kw or eq.energy_source %}
|
|
||||||
<div class="small text-muted">
|
|
||||||
{% if eq.power_kw %}<i class="bi bi-lightning-charge me-1"></i>{{ eq.power_kw }} kW{% endif %}
|
|
||||||
{% if eq.energy_source %}<span class="badge bg-warning-subtle text-dark border ms-1">{{ eq.get_energy_source_display }}</span>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if eq.material or eq.fire_rating or eq.glazing_type %}
|
|
||||||
<div class="small text-muted">
|
|
||||||
{% if eq.material %}{{ eq.material }}{% endif %}
|
|
||||||
{% if eq.glazing_type %} - {{ eq.get_glazing_type_display }}{% endif %}
|
|
||||||
{% if eq.fire_rating %}<span class="badge bg-danger-subtle text-danger border ms-1">{{ eq.fire_rating }}</span>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% if eq.location %}
|
{% if eq.location %}
|
||||||
<a href="{{ eq.location.get_absolute_url }}" class="small text-dark text-decoration-none">
|
<a href="{{ eq.location.get_absolute_url }}" class="small text-dark text-decoration-none">
|
||||||
<i class="bi {{ eq.location.get_icon_class }} me-1"></i>{{ eq.location.code }} - {{ eq.location.get_name }}
|
<i class="bi bi-geo-alt me-1"></i>{{ eq.location.code }} - {{ eq.location.get_name }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="small text-muted">{{ eq.building.code }}</span>
|
<span class="small text-muted">{{ eq.building.code }}</span>
|
||||||
|
|
@ -548,7 +331,7 @@
|
||||||
<td>
|
<td>
|
||||||
{% if eq.parent_asset %}
|
{% if eq.parent_asset %}
|
||||||
<a href="{{ eq.parent_asset.get_absolute_url }}" class="small text-decoration-none">
|
<a href="{{ eq.parent_asset.get_absolute_url }}" class="small text-decoration-none">
|
||||||
<i class="bi {{ eq.parent_asset.get_icon_class }} me-1"></i>{{ eq.parent_asset.code }}
|
<i class="bi bi-diagram-2 me-1"></i>{{ eq.parent_asset.code }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted small">-</span>
|
<span class="text-muted small">-</span>
|
||||||
|
|
@ -560,16 +343,6 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ eq.next_control_date|date:"d/m/Y"|default:"-" }}</td>
|
<td>{{ eq.next_control_date|date:"d/m/Y"|default:"-" }}</td>
|
||||||
<td>
|
|
||||||
{% if eq.contractor_name %}
|
|
||||||
<div class="small fw-semibold"><i class="bi bi-building me-1"></i>{{ eq.contractor_name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if eq.maintenance_contract_ref %}
|
|
||||||
<div class="small text-muted font-monospace">Réf: {{ eq.maintenance_contract_ref }}</div>
|
|
||||||
{% elif not eq.contractor_name %}
|
|
||||||
<span class="text-muted small">-</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-end pe-3">
|
<td class="text-end pe-3">
|
||||||
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap" title="{% translate 'Détail de l’asset' %}">
|
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap" title="{% translate 'Détail de l’asset' %}">
|
||||||
<i class="bi bi-eye me-1"></i>{% translate "Détail" %}
|
<i class="bi bi-eye me-1"></i>{% translate "Détail" %}
|
||||||
|
|
@ -578,7 +351,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" class="text-center text-muted py-4">{% translate "Aucun asset ou équipement technique enregistré." %}</td>
|
<td colspan="7" class="text-center text-muted py-4">{% translate "Aucun asset ou équipement technique enregistré." %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -641,351 +414,87 @@
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const buildingGeom = {{ building_geojson_json|safe }};
|
const buildingGeom = {{ building_geojson_json|safe }};
|
||||||
const centerLon = {{ center_lon_json|safe }} || 4.3572;
|
const centerLon = {{ center_lon|default:4.3572 }};
|
||||||
const centerLat = {{ center_lat_json|safe }} || 50.8477;
|
const centerLat = {{ center_lat|default:50.8477 }};
|
||||||
const floorsData = {{ floors_data_json|safe }} || [];
|
|
||||||
let currentFloor = {{ active_floor_json|safe }};
|
|
||||||
const hasAnyPlan = {{ has_any_plan|yesno:"true,false" }};
|
|
||||||
const levelSpecificMode = {{ level_specific_mode|yesno:"true,false" }};
|
|
||||||
let map = null;
|
let map = null;
|
||||||
|
|
||||||
// 1. Initialisation de la carte MapLibre
|
|
||||||
function initMap() {
|
function initMap() {
|
||||||
const container = document.getElementById("building-map");
|
const container = document.getElementById('building-map');
|
||||||
if (!container || map) return;
|
if (!container) return;
|
||||||
|
|
||||||
map = new maplibregl.Map({
|
map = new maplibregl.Map({
|
||||||
container: "building-map",
|
container: 'building-map',
|
||||||
style: "{{ MAP_STYLE_LIGHT|default:'https://api.maptiler.com/maps/streets-v2-light/style.json' }}",
|
style: '{{ MAP_STYLE_LIGHT|default:"https://api.maptiler.com/maps/streets-v2-light/style.json" }}',
|
||||||
center: [centerLon, centerLat],
|
center: [centerLon, centerLat],
|
||||||
zoom: 16
|
zoom: 16
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl());
|
map.addControl(new maplibregl.NavigationControl());
|
||||||
|
|
||||||
map.on("load", function() {
|
map.on('load', function() {
|
||||||
if (buildingGeom) {
|
if (buildingGeom) {
|
||||||
map.addSource("building-footprint", {
|
map.addSource('building-footprint', {
|
||||||
"type": "geojson",
|
'type': 'geojson',
|
||||||
"data": buildingGeom
|
'data': buildingGeom
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
"id": "building-fill",
|
'id': 'building-fill',
|
||||||
"type": "fill",
|
'type': 'fill',
|
||||||
"source": "building-footprint",
|
'source': 'building-footprint',
|
||||||
"paint": {
|
'paint': {
|
||||||
"fill-color": "#2B579A",
|
'fill-color': '#2B579A',
|
||||||
"fill-opacity": 0.4
|
'fill-opacity': 0.4
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
"id": "building-outline",
|
'id': 'building-outline',
|
||||||
"type": "line",
|
'type': 'line',
|
||||||
"source": "building-footprint",
|
'source': 'building-footprint',
|
||||||
"paint": {
|
'paint': {
|
||||||
"line-color": "#2B579A",
|
'line-color': '#2B579A',
|
||||||
"line-width": 3
|
'line-width': 3
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Bascule entre Mode Plans d'étage et Mode Carte
|
|
||||||
const btnTogglePlans = document.getElementById("btn-toggle-plans");
|
|
||||||
const btnToggleMap = document.getElementById("btn-toggle-map");
|
|
||||||
const sectionPlans = document.getElementById("section-plans");
|
|
||||||
const sectionMap = document.getElementById("section-map");
|
|
||||||
|
|
||||||
function setViewMode(mode) {
|
|
||||||
if (mode === "map") {
|
|
||||||
btnToggleMap?.classList.add("active");
|
|
||||||
btnTogglePlans?.classList.remove("active");
|
|
||||||
if (sectionPlans) sectionPlans.style.display = "none";
|
|
||||||
if (sectionMap) sectionMap.style.display = "block";
|
|
||||||
if (!map) {
|
|
||||||
initMap();
|
initMap();
|
||||||
} else {
|
|
||||||
map.resize();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
btnTogglePlans?.classList.add("active");
|
|
||||||
btnToggleMap?.classList.remove("active");
|
|
||||||
if (sectionPlans) sectionPlans.style.display = "block";
|
|
||||||
if (sectionMap) sectionMap.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
btnTogglePlans?.addEventListener("click", function() { setViewMode("plans"); });
|
const overviewTab = document.getElementById('overview-tab');
|
||||||
btnToggleMap?.addEventListener("click", function() { setViewMode("map"); });
|
|
||||||
|
|
||||||
// Par défaut, si aucun plan n'existe et qu'on n'est pas en mode spécifique à un niveau, afficher la carte
|
|
||||||
if (!hasAnyPlan && !levelSpecificMode && (buildingGeom || (centerLon && centerLat))) {
|
|
||||||
setViewMode("map");
|
|
||||||
} else {
|
|
||||||
setViewMode("plans");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Pan & Zoom interactif sur le viewer de plan
|
|
||||||
const planViewport = document.getElementById("plan-viewport");
|
|
||||||
const planImage = document.getElementById("plan-image");
|
|
||||||
const planWrapper = document.getElementById("plan-viewer-wrapper");
|
|
||||||
const planEmptyState = document.getElementById("plan-empty-state");
|
|
||||||
const planDownloadLink = document.getElementById("plan-download-link");
|
|
||||||
const planActiveName = document.getElementById("plan-active-name");
|
|
||||||
|
|
||||||
let scale = 1;
|
|
||||||
let translateX = 0;
|
|
||||||
let translateY = 0;
|
|
||||||
let isDragging = false;
|
|
||||||
let startX = 0;
|
|
||||||
let startY = 0;
|
|
||||||
|
|
||||||
function updateTransform() {
|
|
||||||
if (planViewport) {
|
|
||||||
planViewport.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPlanView() {
|
|
||||||
scale = 1;
|
|
||||||
translateX = 0;
|
|
||||||
translateY = 0;
|
|
||||||
updateTransform();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("plan-zoom-in")?.addEventListener("click", function() {
|
|
||||||
scale = Math.min(scale * 1.3, 6);
|
|
||||||
updateTransform();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("plan-zoom-out")?.addEventListener("click", function() {
|
|
||||||
scale = Math.max(scale / 1.3, 0.35);
|
|
||||||
updateTransform();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("plan-reset")?.addEventListener("click", resetPlanView);
|
|
||||||
|
|
||||||
document.getElementById("plan-fullscreen")?.addEventListener("click", function() {
|
|
||||||
if (!document.fullscreenElement) {
|
|
||||||
planWrapper?.requestFullscreen?.();
|
|
||||||
} else {
|
|
||||||
document.exitFullscreen?.();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Zoom molette
|
|
||||||
planWrapper?.addEventListener("wheel", function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const delta = e.deltaY > 0 ? 0.88 : 1.14;
|
|
||||||
scale = Math.min(Math.max(scale * delta, 0.35), 6);
|
|
||||||
updateTransform();
|
|
||||||
}, { passive: false });
|
|
||||||
|
|
||||||
// Déplacement souris (pan)
|
|
||||||
planWrapper?.addEventListener("mousedown", function(e) {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
isDragging = true;
|
|
||||||
startX = e.clientX - translateX;
|
|
||||||
startY = e.clientY - translateY;
|
|
||||||
if (planViewport) planViewport.style.cursor = "grabbing";
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("mousemove", function(e) {
|
|
||||||
if (!isDragging) return;
|
|
||||||
translateX = e.clientX - startX;
|
|
||||||
translateY = e.clientY - startY;
|
|
||||||
updateTransform();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("mouseup", function() {
|
|
||||||
if (isDragging) {
|
|
||||||
isDragging = false;
|
|
||||||
if (planViewport) planViewport.style.cursor = "grab";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Navigation entre niveaux / étages
|
|
||||||
function displayFloor(floor) {
|
|
||||||
currentFloor = floor;
|
|
||||||
resetPlanView();
|
|
||||||
|
|
||||||
if (planActiveName) planActiveName.textContent = floor.name;
|
|
||||||
|
|
||||||
// Mise à jour de l'image du plan
|
|
||||||
if (floor.has_plan && floor.plan_url) {
|
|
||||||
if (planImage) {
|
|
||||||
planImage.src = floor.plan_url;
|
|
||||||
planImage.style.display = "block";
|
|
||||||
}
|
|
||||||
if (planEmptyState) planEmptyState.style.display = "none";
|
|
||||||
if (planDownloadLink) {
|
|
||||||
planDownloadLink.href = floor.plan_url;
|
|
||||||
planDownloadLink.style.display = "inline-block";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (planImage) planImage.style.display = "none";
|
|
||||||
if (planEmptyState) planEmptyState.style.display = "block";
|
|
||||||
if (planDownloadLink) planDownloadLink.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mise à jour du bandeau d'information sous le plan
|
|
||||||
const infoTitle = document.getElementById("info-floor-title");
|
|
||||||
const infoCode = document.getElementById("info-floor-code");
|
|
||||||
const infoSurface = document.getElementById("info-floor-surface");
|
|
||||||
const infoRoomsCount = document.getElementById("info-rooms-count");
|
|
||||||
const infoAssetsCount = document.getElementById("info-assets-count");
|
|
||||||
const chipsContainer = document.getElementById("info-floor-rooms-chips");
|
|
||||||
|
|
||||||
if (infoTitle) infoTitle.textContent = floor.name;
|
|
||||||
if (infoCode) infoCode.textContent = `(${floor.code})`;
|
|
||||||
if (infoSurface) {
|
|
||||||
if (floor.surface_m2) {
|
|
||||||
infoSurface.innerHTML = `<i class="bi bi-bounding-box me-1 text-primary"></i>${parseFloat(floor.surface_m2).toFixed(1)} m²`;
|
|
||||||
infoSurface.style.display = "inline-block";
|
|
||||||
} else {
|
|
||||||
infoSurface.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (infoRoomsCount) infoRoomsCount.textContent = floor.rooms_count || 0;
|
|
||||||
if (infoAssetsCount) infoAssetsCount.textContent = floor.assets_count || 0;
|
|
||||||
|
|
||||||
// Locaux de cet étage
|
|
||||||
if (chipsContainer) {
|
|
||||||
chipsContainer.innerHTML = "";
|
|
||||||
if (floor.rooms && floor.rooms.length > 0) {
|
|
||||||
floor.rooms.forEach(function(room) {
|
|
||||||
const chip = document.createElement("a");
|
|
||||||
chip.href = room.url;
|
|
||||||
chip.className = "badge bg-light text-dark border text-decoration-none py-1 px-2";
|
|
||||||
chip.title = room.location_type_display || "";
|
|
||||||
chip.innerHTML = `<i class="bi bi-door-open me-1 text-primary"></i><strong>${room.room_number || room.code}</strong> - ${room.name}`;
|
|
||||||
chipsContainer.appendChild(chip);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
chipsContainer.innerHTML = '<span class="text-muted small fst-italic">{% translate "Aucun sous-local direct répertorié sur ce niveau." %}</span>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Écouteurs sur les boutons d'étages
|
|
||||||
document.querySelectorAll(".floor-nav-btn").forEach(function(btn) {
|
|
||||||
btn.addEventListener("click", function() {
|
|
||||||
const floorId = parseInt(btn.getAttribute("data-floor-id"));
|
|
||||||
const targetFloor = floorsData.find(f => f.id === floorId);
|
|
||||||
if (!targetFloor) return;
|
|
||||||
|
|
||||||
document.querySelectorAll(".floor-nav-btn").forEach(b => {
|
|
||||||
b.classList.remove("btn-primary", "text-white", "shadow-sm");
|
|
||||||
b.classList.add("btn-outline-secondary");
|
|
||||||
});
|
|
||||||
btn.classList.remove("btn-outline-secondary");
|
|
||||||
btn.classList.add("btn-primary", "text-white", "shadow-sm");
|
|
||||||
|
|
||||||
displayFloor(targetFloor);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const overviewTab = document.getElementById("overview-tab");
|
|
||||||
if (overviewTab) {
|
if (overviewTab) {
|
||||||
overviewTab.addEventListener("shown.bs.tab", function () {
|
overviewTab.addEventListener('shown.bs.tab', function () {
|
||||||
if (map) map.resize();
|
if (map) map.resize();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion du chevron plié / déplié dans l'arborescence
|
// Gestion du chevron plié / déplié dans l'arborescence
|
||||||
document.querySelectorAll(".tree-toggle-btn").forEach(function(btn) {
|
document.querySelectorAll('.tree-toggle-btn').forEach(function(btn) {
|
||||||
const targetId = btn.getAttribute("data-bs-target");
|
const targetId = btn.getAttribute('data-bs-target');
|
||||||
const target = document.querySelector(targetId);
|
const target = document.querySelector(targetId);
|
||||||
if (target) {
|
if (target) {
|
||||||
target.addEventListener("show.bs.collapse", function (e) {
|
target.addEventListener('show.bs.collapse', function (e) {
|
||||||
if (e.target !== target) return;
|
if (e.target !== target) return;
|
||||||
const icon = btn.querySelector("i");
|
const icon = btn.querySelector('i');
|
||||||
if (icon) {
|
if (icon) {
|
||||||
icon.classList.remove("bi-chevron-right");
|
icon.classList.remove('bi-chevron-right');
|
||||||
icon.classList.add("bi-chevron-down");
|
icon.classList.add('bi-chevron-down');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
target.addEventListener("hide.bs.collapse", function (e) {
|
target.addEventListener('hide.bs.collapse', function (e) {
|
||||||
if (e.target !== target) return;
|
if (e.target !== target) return;
|
||||||
const icon = btn.querySelector("i");
|
const icon = btn.querySelector('i');
|
||||||
if (icon) {
|
if (icon) {
|
||||||
icon.classList.remove("bi-chevron-down");
|
icon.classList.remove('bi-chevron-down');
|
||||||
icon.classList.add("bi-chevron-right");
|
icon.classList.add('bi-chevron-right');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestion du repliage / dépliage global de l'arborescence
|
|
||||||
const treeCard = document.getElementById("card-hierarchy-tree");
|
|
||||||
if (treeCard) {
|
|
||||||
const collapseAllBtn = document.getElementById("btn-tree-collapse-all");
|
|
||||||
const expandAllBtn = document.getElementById("btn-tree-expand-all");
|
|
||||||
|
|
||||||
if (collapseAllBtn) {
|
|
||||||
collapseAllBtn.addEventListener("click", function() {
|
|
||||||
// Replier tous les nœuds enfants de l'arborescence (localisations et assets)
|
|
||||||
treeCard.querySelectorAll(".tree-collapse-node").forEach(function(el) {
|
|
||||||
if (window.bootstrap && bootstrap.Collapse) {
|
|
||||||
const bsCollapse = bootstrap.Collapse.getOrCreateInstance(el, { toggle: false });
|
|
||||||
bsCollapse.hide();
|
|
||||||
} else {
|
|
||||||
el.classList.remove("show");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Mettre à jour l'état visuel de tous les boutons chevrons
|
|
||||||
treeCard.querySelectorAll(".tree-toggle-btn").forEach(function(btn) {
|
|
||||||
if (btn.getAttribute("data-bs-target") !== "#tree-root-content") {
|
|
||||||
btn.classList.add("collapsed");
|
|
||||||
btn.setAttribute("aria-expanded", "false");
|
|
||||||
const icon = btn.querySelector("i");
|
|
||||||
if (icon) {
|
|
||||||
icon.classList.remove("bi-chevron-down");
|
|
||||||
icon.classList.add("bi-chevron-right");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expandAllBtn) {
|
|
||||||
expandAllBtn.addEventListener("click", function() {
|
|
||||||
// S'assurer que le conteneur racine est bien déplié
|
|
||||||
const rootContent = document.getElementById("tree-root-content");
|
|
||||||
if (rootContent) {
|
|
||||||
if (window.bootstrap && bootstrap.Collapse) {
|
|
||||||
bootstrap.Collapse.getOrCreateInstance(rootContent, { toggle: false }).show();
|
|
||||||
} else {
|
|
||||||
rootContent.classList.add("show");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Déplier tous les nœuds enfants (localisations et assets)
|
|
||||||
treeCard.querySelectorAll(".tree-collapse-node").forEach(function(el) {
|
|
||||||
if (window.bootstrap && bootstrap.Collapse) {
|
|
||||||
const bsCollapse = bootstrap.Collapse.getOrCreateInstance(el, { toggle: false });
|
|
||||||
bsCollapse.show();
|
|
||||||
} else {
|
|
||||||
el.classList.add("show");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Mettre à jour l'état visuel de tous les boutons chevrons
|
|
||||||
treeCard.querySelectorAll(".tree-toggle-btn").forEach(function(btn) {
|
|
||||||
btn.classList.remove("collapsed");
|
|
||||||
btn.setAttribute("aria-expanded", "true");
|
|
||||||
const icon = btn.querySelector("i");
|
|
||||||
if (icon) {
|
|
||||||
icon.classList.remove("bi-chevron-right");
|
|
||||||
icon.classList.add("bi-chevron-down");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
|
|
@ -157,47 +157,6 @@
|
||||||
{{ form.usable_surface_m2 }}
|
{{ form.usable_surface_m2 }}
|
||||||
{% if form.usable_surface_m2.errors %}<div class="text-danger small mt-1">{{ form.usable_surface_m2.errors.0 }}</div>{% endif %}
|
{% if form.usable_surface_m2.errors %}<div class="text-danger small mt-1">{{ form.usable_surface_m2.errors.0 }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-3">
|
|
||||||
<h6 class="border-bottom pb-2 text-primary fw-semibold"><i class="bi bi-door-open me-2"></i>{% translate "Spécificités du local ou du niveau (si applicable)" %}</h6>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="{{ form.room_number.id_for_label }}" class="form-label fw-semibold">{% translate "Numéro de local / porte" %}</label>
|
|
||||||
{{ form.room_number }}
|
|
||||||
{% if form.room_number.errors %}<div class="text-danger small mt-1">{{ form.room_number.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="{{ form.level_number.id_for_label }}" class="form-label fw-semibold">{% translate "Numéro d'étage" %}</label>
|
|
||||||
{{ form.level_number }}
|
|
||||||
{% if form.level_number.errors %}<div class="text-danger small mt-1">{{ form.level_number.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="{{ form.surface_m2.id_for_label }}" class="form-label fw-semibold">{% translate "Surface spécifique (m²)" %}</label>
|
|
||||||
{{ form.surface_m2 }}
|
|
||||||
{% if form.surface_m2.errors %}<div class="text-danger small mt-1">{{ form.surface_m2.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="{{ form.ceiling_height_m.id_for_label }}" class="form-label fw-semibold">{% translate "Hauteur sous plafond (m)" %}</label>
|
|
||||||
{{ form.ceiling_height_m }}
|
|
||||||
{% if form.ceiling_height_m.errors %}<div class="text-danger small mt-1">{{ form.ceiling_height_m.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label for="{{ form.floor_type.id_for_label }}" class="form-label fw-semibold">{% translate "Revêtement de sol" %}</label>
|
|
||||||
{{ form.floor_type }}
|
|
||||||
{% if form.floor_type.errors %}<div class="text-danger small mt-1">{{ form.floor_type.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label for="{{ form.fire_compartment.id_for_label }}" class="form-label fw-semibold">{% translate "Compartiment coupe-feu" %}</label>
|
|
||||||
{{ form.fire_compartment }}
|
|
||||||
{% if form.fire_compartment.errors %}<div class="text-danger small mt-1">{{ form.fire_compartment.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 d-flex align-items-center mt-4">
|
|
||||||
<div class="form-check">
|
|
||||||
{{ form.is_heated }}
|
|
||||||
<label for="{{ form.is_heated.id_for_label }}" class="form-check-label fw-semibold">
|
|
||||||
{% translate "Local chauffé" %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label small text-muted">{% translate "Type de localisation" %}</label>
|
|
||||||
<select name="location_type" class="form-select form-select-sm">
|
|
||||||
<option value="">-- {% translate "Tous les types" %} --</option>
|
|
||||||
{% for key, label in location_type_choices %}
|
|
||||||
<option value="{{ key }}" {% if selected_location_type == key %}selected{% endif %}>{{ label }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label small text-muted">{% translate "Statut" %}</label>
|
<label class="form-label small text-muted">{% translate "Statut" %}</label>
|
||||||
<select name="building_status" class="form-select form-select-sm">
|
<select name="building_status" class="form-select form-select-sm">
|
||||||
|
|
@ -56,16 +47,16 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1">
|
<div class="col-md-2">
|
||||||
<label class="form-label small text-muted">{% translate "DPE" %}</label>
|
<label class="form-label small text-muted">{% translate "Diagnostic DPE" %}</label>
|
||||||
<select name="dpe_rating" class="form-select form-select-sm">
|
<select name="dpe_rating" class="form-select form-select-sm">
|
||||||
<option value="">-- {% translate "Tous" %} --</option>
|
<option value="">-- {% translate "Tous DPE" %} --</option>
|
||||||
{% for key, label in dpe_rating_choices %}
|
{% for key, label in dpe_rating_choices %}
|
||||||
<option value="{{ key }}" {% if selected_dpe == key %}selected{% endif %}>{{ label }}</option>
|
<option value="{{ key }}" {% if selected_dpe == key %}selected{% endif %}>DPE {{ label }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 d-flex align-items-end gap-2">
|
<div class="col-md-3 d-flex align-items-end gap-2">
|
||||||
<button type="submit" class="btn btn-sm btn-primary w-100"><i class="bi bi-filter me-1"></i>{% translate "Filtrer" %}</button>
|
<button type="submit" class="btn btn-sm btn-primary w-100"><i class="bi bi-filter me-1"></i>{% translate "Filtrer" %}</button>
|
||||||
<a href="{% url 'assets:buildings_index' %}" class="btn btn-sm btn-outline-secondary w-100">{% translate "Réinitialiser" %}</a>
|
<a href="{% url 'assets:buildings_index' %}" class="btn btn-sm btn-outline-secondary w-100">{% translate "Réinitialiser" %}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -95,11 +86,8 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'assets:publicbuilding_detail' b.pk %}" class="fw-bold text-decoration-none">
|
<a href="{% url 'assets:publicbuilding_detail' b.pk %}" class="fw-bold text-decoration-none">
|
||||||
<i class="bi {{ b.get_icon_class }} me-1 text-primary"></i>{{ b.code }}
|
{{ b.code }}
|
||||||
</a>
|
</a>
|
||||||
{% if b.room_number %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary border small font-monospace"><i class="bi bi-tag me-1"></i>{{ b.room_number }}</span>
|
|
||||||
{% endif %}
|
|
||||||
<div class="small text-muted">{{ b.get_name }}</div>
|
<div class="small text-muted">{{ b.get_name }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -15,41 +15,52 @@
|
||||||
<i class="bi bi-chevron-down small"></i>
|
<i class="bi bi-chevron-down small"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="d-inline-block" style="width: 1.35rem;"></span>
|
<span class="px-2"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<i class="bi {{ child_node.loc.get_icon_class }} text-primary"></i>
|
{% if child_node.loc.location_type == 'building' or child_node.loc.location_type == 'site' %}
|
||||||
|
<i class="bi bi-building text-primary"></i>
|
||||||
|
{% elif child_node.loc.location_type == 'level' %}
|
||||||
|
<i class="bi bi-layers text-secondary"></i>
|
||||||
|
{% elif child_node.loc.location_type == 'room' %}
|
||||||
|
<i class="bi bi-door-open text-info"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-geo-alt text-dark"></i>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
|
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
|
||||||
<a href="{{ child_node.loc.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
|
<a href="{{ child_node.loc.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
|
||||||
{{ child_node.loc.code }}
|
{{ child_node.loc.code }}
|
||||||
</a>
|
</a>
|
||||||
{% if child_node.loc.room_number %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary border small font-monospace"><i class="bi bi-tag me-1"></i>{{ child_node.loc.room_number }}</span>
|
|
||||||
{% endif %}
|
|
||||||
<span class="small text-muted">- {{ child_node.loc.get_name }}</span>
|
<span class="small text-muted">- {{ child_node.loc.get_name }}</span>
|
||||||
<span class="badge bg-primary-subtle text-primary border small">{{ child_node.loc.get_location_type_display }}</span>
|
<span class="badge bg-primary-subtle text-primary border small">{{ child_node.loc.get_location_type_display }}</span>
|
||||||
|
|
||||||
{% if child_node.assets %}
|
{% if child_node.assets %}
|
||||||
<span class="badge bg-info-subtle text-info border small" title="{% translate 'Assets physiques rattachés' %}">
|
<button class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap"
|
||||||
<i class="bi bi-tools me-1"></i>{{ child_node.assets|length }}
|
type="button"
|
||||||
</span>
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#tree-assets-loc-{{ child_node.loc.id }}"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="bi bi-tools me-1"></i>Assets ({{ child_node.assets|length }})
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Collapsible Content (Direct Assets & Child Locations) -->
|
<!-- Collapsible Assets for this location (NON VISIBLES PAR DÉFAUT) -->
|
||||||
{% if child_node.children or child_node.assets %}
|
|
||||||
<div id="tree-loc-{{ child_node.loc.id }}" class="collapse show tree-collapse-node">
|
|
||||||
<!-- 1. Assets directement rattachés à cette localisation -->
|
|
||||||
{% if child_node.assets %}
|
{% if child_node.assets %}
|
||||||
|
<div id="tree-assets-loc-{{ child_node.loc.id }}" class="collapse mt-2" style="padding-left: calc({{ depth|default:1 }} * 1.25rem);">
|
||||||
|
<div class="card card-body bg-light border-info-subtle p-2 mb-2 me-3">
|
||||||
|
<div class="fw-bold small text-info mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ child_node.loc.code }} :</div>
|
||||||
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=child_node.assets depth=depth|default:1|add:1 only %}
|
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=child_node.assets depth=depth|default:1|add:1 only %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- 2. Sous-localisations -->
|
<!-- Collapsible Child Locations -->
|
||||||
{% if child_node.children %}
|
{% if child_node.children %}
|
||||||
|
<div id="tree-loc-{{ child_node.loc.id }}" class="collapse show">
|
||||||
{% include "assets/buildings/publicbuilding_location_tree.html" with node=child_node depth=depth|default:1|add:1 only %}
|
{% include "assets/buildings/publicbuilding_location_tree.html" with node=child_node depth=depth|default:1|add:1 only %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% if insp.notes and not insp.is_giep %}
|
{% if insp.notes %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-muted small fw-semibold"><i class="bi bi-journal-text me-1"></i>{% translate "Remarques / Observations" %}:</label>
|
<label class="form-label text-muted small fw-semibold"><i class="bi bi-journal-text me-1"></i>{% translate "Remarques / Observations" %}:</label>
|
||||||
<div class="p-2 bg-light rounded border text-secondary small">
|
<div class="p-2 bg-light rounded border text-secondary small">
|
||||||
|
|
@ -162,7 +162,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if insp.documents.all and not insp.is_giep %}
|
{% if insp.documents.all %}
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label text-muted small fw-semibold mb-2"><i class="bi bi-camera me-1"></i>{% translate "Photos & Documents joints" %} ({{ insp.documents.count }}):</label>
|
<label class="form-label text-muted small fw-semibold mb-2"><i class="bi bi-camera me-1"></i>{% translate "Photos & Documents joints" %} ({{ insp.documents.count }}):</label>
|
||||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
|
|
|
||||||
|
|
@ -85,9 +85,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="pe-3">
|
<td class="pe-3">
|
||||||
<small class="text-muted" title="{% if intervention.end_time %}{% translate 'Date de réalisation' %}{% elif intervention.expected_end_time %}{% translate 'Échéance prévisionnelle' %}{% elif intervention.expected_begin_time %}{% translate 'Date de début prévisionnelle' %}{% else %}{% translate 'Date de création' %}{% endif %}">
|
<small class="text-muted">{{ intervention.creation_time|date:"d/m/Y" }}</small>
|
||||||
{{ intervention.end_time|default:intervention.expected_end_time|default:intervention.expected_begin_time|default:intervention.creation_time|date:"d/m/Y" }}
|
|
||||||
</small>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -168,7 +166,7 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">{% translate "Description / Remarques" %}</label>
|
<label class="form-label fw-semibold">{% translate "Description / Remarques" %}</label>
|
||||||
<textarea name="init_description" class="form-control" rows="3" placeholder="{% translate 'Détails ou remarques globales...' %}"></textarea>
|
<textarea name="init_description" class="form-control" rows="3" placeholder="{% translate 'Détails ou remarques initiales...' %}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer bg-light">
|
<div class="modal-footer bg-light">
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<div id="quickInspectionAlert" class="alert alert-danger d-none mb-3"></div>
|
<div id="quickInspectionAlert" class="alert alert-danger d-none mb-3"></div>
|
||||||
|
|
||||||
<!-- Résultat de contrôle avec boutons colorés -->
|
<!-- Résultat de contrôle avec boutons colorés -->
|
||||||
<div class="mb-3" id="quickInspResultSection">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold d-block">{% translate "Résultat du contrôle" %} <span class="text-danger">*</span></label>
|
<label class="form-label fw-semibold d-block">{% translate "Résultat du contrôle" %} <span class="text-danger">*</span></label>
|
||||||
<div class="d-flex flex-column gap-2" role="group" aria-label="{% translate 'Résultat' %}">
|
<div class="d-flex flex-column gap-2" role="group" aria-label="{% translate 'Résultat' %}">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section Photos (Mode continu & Fichiers) -->
|
<!-- Section Photos (Mode continu & Fichiers) -->
|
||||||
<div class="card border border-success-subtle mb-3" style="background-color: #e8f6f3;" id="quickInspPhotosSection">
|
<div class="card border border-success-subtle mb-3" style="background-color: #e8f6f3;">
|
||||||
<div class="card-header bg-transparent border-0 pb-0 pt-2 px-3">
|
<div class="card-header bg-transparent border-0 pb-0 pt-2 px-3">
|
||||||
<span class="fw-bold text-dark small"><i class="bi bi-camera me-1 text-teal"></i>{% translate "Photos & Fichiers" %}</span>
|
<span class="fw-bold text-dark small"><i class="bi bi-camera me-1 text-teal"></i>{% translate "Photos & Fichiers" %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -124,7 +124,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Remarques -->
|
<!-- Remarques -->
|
||||||
<div class="mb-3" id="quickInspNotesSection">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold mb-1">{% translate "Remarques / Remarques du constat" %}</label>
|
<label class="form-label fw-semibold mb-1">{% translate "Remarques / Remarques du constat" %}</label>
|
||||||
<textarea name="notes" id="insp_notes" class="form-control" rows="2" placeholder="{% translate 'Saisir vos remarques ou détails...' %}"></textarea>
|
<textarea name="notes" id="insp_notes" class="form-control" rows="2" placeholder="{% translate 'Saisir vos remarques ou détails...' %}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -534,19 +534,6 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 -->
|
<!-- Champs spécifiques au type d'asset -->
|
||||||
{% get_asset_specific_fields asset as asset_specific_fields %}
|
{% get_asset_specific_fields asset as asset_specific_fields %}
|
||||||
{% if asset_specific_fields %}
|
{% if asset_specific_fields %}
|
||||||
|
|
@ -1852,11 +1839,11 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<!-- MapLibre GL 5.7.0 -->
|
<!-- MapLibre GL 5.7.0 -->
|
||||||
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
||||||
<script src="{% static 'common/layer-switcher-control.js' %}?v=20261010_order"></script>
|
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
||||||
<script src="{% static 'assets/asset_cms_edit_mode.js' %}"></script>
|
<script src="{% static 'assets/asset_cms_edit_mode.js' %}"></script>
|
||||||
<script src="{% static 'assets/asset_location_edit.js' %}"></script>
|
<script src="{% static 'assets/asset_location_edit.js' %}"></script>
|
||||||
<!-- MapLibre map initialization for asset detail -->
|
<!-- MapLibre map initialization for asset detail -->
|
||||||
<script src="{% static 'assets/asset_generic_map.js' %}?v=20261010_order"></script>
|
<script src="{% static 'assets/asset_generic_map.js' %}"></script>
|
||||||
<!-- MapboxDraw for geometry editing (Point, LineString, Polygon) -->
|
<!-- MapboxDraw for geometry editing (Point, LineString, Polygon) -->
|
||||||
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
||||||
<script src="{% static 'assets/asset_geometry_edit.js' %}?v=1.1"></script>
|
<script src="{% static 'assets/asset_geometry_edit.js' %}?v=1.1"></script>
|
||||||
|
|
@ -1865,11 +1852,11 @@
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
<!-- MapLibre GL 5.7.0 -->
|
<!-- MapLibre GL 5.7.0 -->
|
||||||
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
||||||
<script src="{% static 'common/layer-switcher-control.js' %}?v=20261010_order"></script>
|
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
||||||
<script src="{% static 'assets/asset_cms_edit_mode.js' %}"></script>
|
<script src="{% static 'assets/asset_cms_edit_mode.js' %}"></script>
|
||||||
<script src="{% static 'assets/asset_location_edit.js' %}"></script>
|
<script src="{% static 'assets/asset_location_edit.js' %}"></script>
|
||||||
<!-- MapLibre map initialization for asset detail -->
|
<!-- MapLibre map initialization for asset detail -->
|
||||||
<script src="{% static 'assets/asset_generic_map.js' %}?v=20261010_order"></script>
|
<script src="{% static 'assets/asset_generic_map.js' %}"></script>
|
||||||
<!-- MapboxDraw for geometry editing (Point, LineString, Polygon) -->
|
<!-- MapboxDraw for geometry editing (Point, LineString, Polygon) -->
|
||||||
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
||||||
<script src="{% static 'assets/asset_geometry_edit.js' %}?v=1.1"></script>
|
<script src="{% static 'assets/asset_geometry_edit.js' %}?v=1.1"></script>
|
||||||
|
|
|
||||||
|
|
@ -111,11 +111,6 @@
|
||||||
id: 'assets',
|
id: 'assets',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: 'assets',
|
source: 'assets',
|
||||||
filter: [
|
|
||||||
'any',
|
|
||||||
['==', ['geometry-type'], 'Point'],
|
|
||||||
['==', ['geometry-type'], 'MultiPoint']
|
|
||||||
],
|
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': [
|
'circle-radius': [
|
||||||
'interpolate',
|
'interpolate',
|
||||||
|
|
@ -140,32 +135,7 @@
|
||||||
minzoom: 8
|
minzoom: 8
|
||||||
};
|
};
|
||||||
|
|
||||||
const assets_lines_layer_def = {
|
const assets_layers_defs = [assets_layer_def];
|
||||||
id: 'assets-lines',
|
|
||||||
type: 'line',
|
|
||||||
source: 'assets',
|
|
||||||
filter: [
|
|
||||||
'any',
|
|
||||||
['==', ['geometry-type'], 'LineString'],
|
|
||||||
['==', ['geometry-type'], 'MultiLineString']
|
|
||||||
],
|
|
||||||
paint: {
|
|
||||||
'line-color': [
|
|
||||||
'case',
|
|
||||||
['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b',
|
|
||||||
'#0022ff'
|
|
||||||
],
|
|
||||||
'line-width': [
|
|
||||||
'case',
|
|
||||||
['boolean', ['feature-state', 'highlighted'], false], 6,
|
|
||||||
4
|
|
||||||
],
|
|
||||||
'line-opacity': 0.85
|
|
||||||
},
|
|
||||||
minzoom: 8
|
|
||||||
};
|
|
||||||
|
|
||||||
const assets_layers_defs = [assets_lines_layer_def, assets_layer_def];
|
|
||||||
|
|
||||||
|
|
||||||
// Filtrer les modèles par catégorie dans le formulaire des assets
|
// Filtrer les modèles par catégorie dans le formulaire des assets
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% load static i18n %}
|
|
||||||
|
|
||||||
{% block title %}{% translate "Supprimer la zone de gestion" %} {{ zone.code }} | {{ product_name }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container py-5" style="max-width: 600px;">
|
|
||||||
<div class="card shadow border-danger">
|
|
||||||
<div class="card-header bg-danger text-white py-3">
|
|
||||||
<h5 class="card-title mb-0">
|
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>{% translate "Confirmer la suppression" %}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<p>{% translate "Êtes-vous certain de vouloir supprimer définitivement la zone de gestion suivante ?" %}</p>
|
|
||||||
<div class="alert alert-light border">
|
|
||||||
<h6 class="fw-bold mb-1">{{ zone.code }} - {{ zone.get_name }}</h6>
|
|
||||||
<div class="small text-muted">{{ zone.thematic.name }} {% if zone.zone_type %}| {{ zone.zone_type.get_name }}{% endif %}</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-danger small">
|
|
||||||
<i class="bi bi-info-circle me-1"></i>{% translate "Cette action est irréversible. Les documents et associations liés à cette zone seront supprimés." %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="post" action="{% url 'assets:management_zone_delete' zone.pk %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="d-flex justify-content-end gap-2 mt-4">
|
|
||||||
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="btn btn-outline-secondary">
|
|
||||||
{% translate "Annuler" %}
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-danger">
|
|
||||||
<i class="bi bi-trash me-1"></i>{% translate "Supprimer définitivement" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
@ -1,434 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% load static i18n %}
|
|
||||||
|
|
||||||
{% block title %}{{ zone.code }} - {{ zone.get_name }} | {{ 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"><a href="{% url 'assets:management_zones_index' %}?thematic={{ zone.thematic.code }}">{% translate "Zones de gestion" %}</a></li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">{{ zone.code }}</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<a href="{% url 'assets:management_zones_index' %}?thematic={{ zone.thematic.code }}" class="btn btn-outline-secondary btn-sm">
|
|
||||||
<i class="bi bi-arrow-left me-1"></i>{% translate "Retour à la liste" %}
|
|
||||||
</a>
|
|
||||||
{% if can_edit %}
|
|
||||||
<a href="{% url 'assets:management_zone_edit' zone.pk %}" class="btn btn-outline-primary btn-sm">
|
|
||||||
<i class="bi bi-pencil me-1"></i>{% translate "Modifier" %}
|
|
||||||
</a>
|
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteZoneModal">
|
|
||||||
<i class="bi bi-trash me-1"></i>{% translate "Supprimer" %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Zone Banner Card -->
|
|
||||||
<div class="card shadow-sm mb-4 border-start border-4" style="border-left-color: {{ zone.get_color }} !important;">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="d-flex align-items-center gap-2 mb-2">
|
|
||||||
<span style="display:inline-block; width: 16px; height: 16px; border-radius: 4px; background-color: {{ zone.get_color }}; border: 1px solid rgba(0,0,0,0.25);"></span>
|
|
||||||
<h1 class="h4 mb-0 fw-bold">{{ zone.code }} - {{ zone.get_name }}</h1>
|
|
||||||
</div>
|
|
||||||
{% if zone.name_nl and zone.name_nl != zone.name_fr %}
|
|
||||||
<div class="text-muted small mb-2 fst-italic">{{ zone.name_nl }}</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary border">
|
|
||||||
<i class="bi bi-tag me-1"></i>{{ zone.thematic.name }}
|
|
||||||
</span>
|
|
||||||
{% if zone.zone_type %}
|
|
||||||
<span class="badge" style="background-color: {{ zone.zone_type.default_color }}; color: #fff;">
|
|
||||||
{{ zone.zone_type.get_name }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if zone.is_active %}
|
|
||||||
<span class="badge bg-success-subtle text-success border">
|
|
||||||
<i class="bi bi-check-circle me-1"></i>{% translate "Active" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger-subtle text-danger border">
|
|
||||||
<i class="bi bi-dash-circle me-1"></i>{% translate "Inactive" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if area_ha %}
|
|
||||||
<span class="badge bg-light text-dark border">
|
|
||||||
<i class="bi bi-rulers me-1"></i>{{ area_ha }} ha ({{ area_m2|floatformat:0 }} m²)
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 text-md-end mt-3 mt-md-0">
|
|
||||||
{% if zone.target_frequency_days %}
|
|
||||||
<div class="d-inline-block text-center me-3 p-2 bg-light rounded border">
|
|
||||||
<span class="d-block text-muted small">{% translate "Fréquence cible" %}</span>
|
|
||||||
<span class="fs-5 fw-bold text-dark">{{ zone.target_frequency_days }} {% translate "j" %}</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if zone.seasonality_notes %}
|
|
||||||
<div class="d-inline-block text-start p-2 bg-light rounded border">
|
|
||||||
<span class="d-block text-muted small">{% translate "Période" %}</span>
|
|
||||||
<span class="small fw-semibold text-dark">{{ zone.seasonality_notes }}</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Consignes et Points d'attention (Callout Banner) -->
|
|
||||||
{% if zone.instructions %}
|
|
||||||
<div class="card shadow-sm mb-4 border-warning border-start border-4">
|
|
||||||
<div class="card-body bg-warning bg-opacity-10">
|
|
||||||
<div class="d-flex align-items-start gap-2">
|
|
||||||
<i class="bi bi-exclamation-triangle-fill text-warning fs-4 mt-1"></i>
|
|
||||||
<div>
|
|
||||||
<h5 class="h6 fw-bold text-dark mb-1">{% translate "Consignes d'exploitation et points de vigilance" %}</h5>
|
|
||||||
<div class="text-dark" style="white-space: pre-line;">{{ zone.instructions }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Main Grid: Informations & Carte -->
|
|
||||||
<div class="row g-4 mb-4">
|
|
||||||
<!-- Informations & Modalités -->
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="card shadow-sm h-100">
|
|
||||||
<div class="card-header bg-light fw-bold">
|
|
||||||
<i class="bi bi-info-circle me-2 text-success"></i>{% translate "Détails & Caractéristiques" %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row mb-0">
|
|
||||||
<dt class="col-sm-4 text-muted">{% translate "Description" %}</dt>
|
|
||||||
<dd class="col-sm-8">{{ zone.description|default:_("Aucune description renseignée")|linebreaks }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 text-muted">{% translate "Thématique" %}</dt>
|
|
||||||
<dd class="col-sm-8">{{ zone.thematic.name }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 text-muted">{% translate "Type de zone" %}</dt>
|
|
||||||
<dd class="col-sm-8">{{ zone.zone_type.get_name|default:"-" }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 text-muted">{% translate "Couleur de repérage" %}</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<span style="display:inline-block; width:18px; height:18px; border-radius:3px; background-color: {{ zone.get_color }}; border: 1px solid rgba(0,0,0,0.25);"></span>
|
|
||||||
<code>{{ zone.get_color }}</code>
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 text-muted">{% translate "Superficie estimée" %}</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
{% if area_m2 %}
|
|
||||||
<strong>{{ area_ha }} hectares</strong> ({{ area_m2|floatformat:0 }} m²)
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">{% translate "Périmètre non défini" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 text-muted">{% translate "Créé par" %}</dt>
|
|
||||||
<dd class="col-sm-8">{% if zone.created_by %}{{ zone.created_by.get_full_name|default:zone.created_by.username }}{% else %}-{% endif %}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 text-muted">{% translate "Date de création" %}</dt>
|
|
||||||
<dd class="col-sm-8">{{ zone.created_at|date:"d/m/Y H:i" }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 text-muted">{% translate "Dernière mise à jour" %}</dt>
|
|
||||||
<dd class="col-sm-8">{{ zone.updated_at|date:"d/m/Y H:i" }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Carte du périmètre géographique -->
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="card shadow-sm h-100">
|
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
||||||
<span class="fw-bold">
|
|
||||||
<i class="bi bi-map me-2 text-success"></i>{% translate "Périmètre géographique" %}
|
|
||||||
</span>
|
|
||||||
{% if zone.geom %}
|
|
||||||
<span class="badge bg-success">{% translate "Géométrie définie" %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">{% translate "Aucun périmètre" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0 position-relative">
|
|
||||||
<div id="detailZoneMap" style="height: 380px; width: 100%;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Documents & Arrêtés -->
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
||||||
<span class="fw-bold">
|
|
||||||
<i class="bi bi-files me-2 text-success"></i>{% translate "Documents & Fiches techniques" %} ({{ documents.count }})
|
|
||||||
</span>
|
|
||||||
{% if can_edit %}
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-success" data-bs-toggle="modal" data-bs-target="#uploadDocModal">
|
|
||||||
<i class="bi bi-upload me-1"></i>{% translate "Ajouter des documents" %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
{% if documents %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover align-middle mb-0">
|
|
||||||
<thead class="table-light small text-uppercase text-muted">
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Fichier" %}</th>
|
|
||||||
<th>{% translate "Date d'ajout" %}</th>
|
|
||||||
<th>{% translate "Ajouté par" %}</th>
|
|
||||||
<th class="text-end" style="width: 140px;">{% translate "Actions" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for doc in documents %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<i class="bi bi-file-earmark-text text-primary fs-5"></i>
|
|
||||||
<div>
|
|
||||||
<a href="{{ doc.file.url }}" target="_blank" class="fw-semibold text-decoration-none">
|
|
||||||
{{ doc.file.name|slice:"25:"|default:doc.file.name }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{{ doc.upload_date|date:"d/m/Y H:i" }}</td>
|
|
||||||
<td>{% if doc.uploaded_by %}{{ doc.uploaded_by.get_full_name|default:doc.uploaded_by.username }}{% else %}-{% endif %}</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<a href="{{ doc.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="{% translate 'Télécharger' %}">
|
|
||||||
<i class="bi bi-download"></i>
|
|
||||||
</a>
|
|
||||||
{% if can_edit %}
|
|
||||||
<form method="post" action="{% url 'assets:management_zone_delete_document' zone.pk doc.pk %}" class="d-inline" onsubmit="return confirm('{% translate 'Supprimer ce document ?' %}');">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="{% translate 'Supprimer' %}">
|
|
||||||
<i class="bi bi-trash"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="p-4 text-center text-muted small">
|
|
||||||
<i class="bi bi-folder2-open fs-2 d-block mb-2 text-muted"></i>
|
|
||||||
{% translate "Aucun document n'a été rattaché à cette zone de gestion." %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Équipements rattachés (Assets explicites) -->
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<span class="fw-bold">
|
|
||||||
<i class="bi bi-layers me-2 text-success"></i>{% translate "Équipements rattachés explicitement" %} ({{ explicit_assets.count }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
{% if explicit_assets %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover align-middle mb-0">
|
|
||||||
<thead class="table-light small text-uppercase text-muted">
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Équipement" %}</th>
|
|
||||||
<th>{% translate "Type" %}</th>
|
|
||||||
<th>{% translate "Statut de rattachement" %}</th>
|
|
||||||
<th>{% translate "Notes" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for ea in explicit_assets %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong>{{ ea.asset|default:ea.object_id }}</strong>
|
|
||||||
</td>
|
|
||||||
<td>{{ ea.content_type.name }}</td>
|
|
||||||
<td>
|
|
||||||
{% if ea.is_excluded %}
|
|
||||||
<span class="badge bg-danger-subtle text-danger border">
|
|
||||||
<i class="bi bi-slash-circle me-1"></i>{% translate "Exclu de la zone" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-success-subtle text-success border">
|
|
||||||
<i class="bi bi-check-circle me-1"></i>{% translate "Inclus dans la zone" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-muted small">{{ ea.notes|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="p-4 text-center text-muted small">
|
|
||||||
<p class="mb-0">{% translate "Tous les équipements situés dans le périmètre géographique de la zone y sont automatiquement associés par intersection spatiale." %}</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal: Upload Documents -->
|
|
||||||
{% if can_edit and doc_form %}
|
|
||||||
<div class="modal fade" id="uploadDocModal" tabindex="-1" aria-labelledby="uploadDocModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="post" action="{% url 'assets:management_zone_add_document' zone.pk %}" enctype="multipart/form-data">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="uploadDocModalLabel">
|
|
||||||
<i class="bi bi-upload me-2 text-success"></i>{% translate "Ajouter des documents" %}
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ doc_form.documents.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Sélectionner des fichiers" %}
|
|
||||||
</label>
|
|
||||||
{{ doc_form.documents }}
|
|
||||||
<div class="form-text small">
|
|
||||||
{% translate "Formats acceptés : PDF, Word, Excel, images, etc. Vous pouvez sélectionner plusieurs fichiers simultanément." %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
|
||||||
<button type="submit" class="btn btn-success">
|
|
||||||
<i class="bi bi-check-lg me-1"></i>{% translate "Téléverser" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Modal: Delete Zone Confirmation -->
|
|
||||||
{% if can_edit %}
|
|
||||||
<div class="modal fade" id="deleteZoneModal" tabindex="-1" aria-labelledby="deleteZoneModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="post" action="{% url 'assets:management_zone_delete' zone.pk %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="modal-header bg-danger text-white">
|
|
||||||
<h5 class="modal-title" id="deleteZoneModalLabel">
|
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>{% translate "Confirmer la suppression" %}
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>{% translate "Êtes-vous certain de vouloir supprimer la zone de gestion suivante ?" %}</p>
|
|
||||||
<div class="alert alert-light border">
|
|
||||||
<strong>{{ zone.code }}</strong> - {{ zone.get_name }}<br>
|
|
||||||
<small class="text-muted">{{ zone.thematic.name }} | {{ zone.zone_type.get_name|default:"-" }}</small>
|
|
||||||
</div>
|
|
||||||
<p class="text-danger small mb-0">
|
|
||||||
<i class="bi bi-info-circle me-1"></i>{% translate "Cette action est irréversible. Les documents et historiques liés seront supprimés." %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
|
||||||
<button type="submit" class="btn btn-danger">
|
|
||||||
<i class="bi bi-trash me-1"></i>{% translate "Supprimer définitivement" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
const mapDiv = document.getElementById("detailZoneMap");
|
|
||||||
if (!mapDiv || typeof maplibregl === "undefined") return;
|
|
||||||
|
|
||||||
const zoneColor = "{{ zone.get_color }}";
|
|
||||||
const geojsonGeometry = {% if zone.geojson_geometry %}{{ zone.geojson_geometry|safe }}{% else %}null{% endif %};
|
|
||||||
|
|
||||||
const map = new maplibregl.Map({
|
|
||||||
container: mapDiv,
|
|
||||||
style: window.MAP_STYLE_LIGHT,
|
|
||||||
center: window.MAP_DEFAULT_CENTER || [4.3572, 50.8477],
|
|
||||||
zoom: 12
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
|
||||||
|
|
||||||
map.on('load', function () {
|
|
||||||
if (!geojsonGeometry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const feature = {
|
|
||||||
type: "Feature",
|
|
||||||
geometry: geojsonGeometry
|
|
||||||
};
|
|
||||||
|
|
||||||
map.addSource("detail-zone-source", {
|
|
||||||
type: "geojson",
|
|
||||||
data: feature
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addLayer({
|
|
||||||
id: "detail-zone-fill",
|
|
||||||
type: "fill",
|
|
||||||
source: "detail-zone-source",
|
|
||||||
paint: {
|
|
||||||
"fill-color": zoneColor,
|
|
||||||
"fill-opacity": 0.30
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addLayer({
|
|
||||||
id: "detail-zone-line",
|
|
||||||
type: "line",
|
|
||||||
source: "detail-zone-source",
|
|
||||||
paint: {
|
|
||||||
"line-color": zoneColor,
|
|
||||||
"line-width": 3,
|
|
||||||
"line-dasharray": [3, 1.5]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fit bounds to polygon
|
|
||||||
const bounds = new maplibregl.LngLatBounds();
|
|
||||||
const coords = geojsonGeometry.coordinates;
|
|
||||||
const addCoords = (arr) => {
|
|
||||||
if (typeof arr[0] === 'number') {
|
|
||||||
bounds.extend(arr);
|
|
||||||
} else {
|
|
||||||
arr.forEach(addCoords);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
addCoords(coords);
|
|
||||||
|
|
||||||
if (!bounds.isEmpty()) {
|
|
||||||
map.fitBounds(bounds, { padding: 45, maxZoom: 16 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock script %}
|
|
||||||
|
|
@ -1,568 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% load static i18n %}
|
|
||||||
|
|
||||||
{% block title %}{{ title }} | {{ 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"><a href="{% url 'assets:management_zones_index' %}">{% translate "Zones de gestion" %}</a></li>
|
|
||||||
{% if not is_create and zone %}
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'assets:management_zone_detail' zone.pk %}">{{ zone.code }}</a></li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">{% translate "Modifier" %}</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">{% translate "Nouvelle zone" %}</li>
|
|
||||||
{% endif %}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
<div>
|
|
||||||
{% if not is_create and zone %}
|
|
||||||
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="btn btn-outline-secondary btn-sm">
|
|
||||||
<i class="bi bi-arrow-left me-1"></i>{% translate "Annuler" %}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'assets:management_zones_index' %}" class="btn btn-outline-secondary btn-sm">
|
|
||||||
<i class="bi bi-arrow-left me-1"></i>{% translate "Annuler" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<h1 class="h3 mb-1">
|
|
||||||
{% if is_create %}
|
|
||||||
<i class="bi bi-plus-circle-fill text-success me-2"></i>{% translate "Créer une zone de gestion" %}
|
|
||||||
{% else %}
|
|
||||||
<i class="bi bi-pencil-square text-success me-2"></i>{% translate "Modifier la zone de gestion" %} {{ zone.code }}
|
|
||||||
{% endif %}
|
|
||||||
</h1>
|
|
||||||
<p class="text-muted mb-0">
|
|
||||||
{% translate "Définissez les paramètres d'entretien, les consignes et le périmètre géographique de la zone." %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if form.errors %}
|
|
||||||
<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
|
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
|
||||||
<strong>{% translate "Certains champs contiennent des erreurs." %}</strong>
|
|
||||||
{% translate "Veuillez vérifier les informations ci-dessous." %}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form method="post" action="" class="needs-validation" id="managementZoneForm" novalidate>
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.geom_geojson }}
|
|
||||||
|
|
||||||
<div class="row g-4">
|
|
||||||
<!-- Left Column: Form Fields -->
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<!-- Informations générales -->
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header bg-light fw-bold">
|
|
||||||
<i class="bi bi-info-circle me-2 text-success"></i>{% translate "Informations générales" %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<label for="{{ form.code.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Code de la zone" %} <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.code }}
|
|
||||||
{% if form.code.errors %}<div class="text-danger small mt-1">{{ form.code.errors.0 }}</div>{% endif %}
|
|
||||||
<div class="form-text small">{% translate "Identifiant unique (ex: ZG-NAT-001)" %}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-7">
|
|
||||||
<label for="{{ form.thematic.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Thématique" %} <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.thematic }}
|
|
||||||
{% if form.thematic.errors %}<div class="text-danger small mt-1">{{ form.thematic.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="{{ form.name_fr.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Nom (FR)" %} <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.name_fr }}
|
|
||||||
{% if form.name_fr.errors %}<div class="text-danger small mt-1">{{ form.name_fr.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="{{ form.name_nl.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Nom (NL)" %}
|
|
||||||
</label>
|
|
||||||
{{ form.name_nl }}
|
|
||||||
{% if form.name_nl.errors %}<div class="text-danger small mt-1">{{ form.name_nl.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-8">
|
|
||||||
<label for="{{ form.zone_type.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Type de zone de gestion" %}
|
|
||||||
</label>
|
|
||||||
{{ form.zone_type }}
|
|
||||||
{% if form.zone_type.errors %}<div class="text-danger small mt-1">{{ form.zone_type.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label for="{{ form.color.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Couleur" %}
|
|
||||||
</label>
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
{{ form.color }}
|
|
||||||
<span class="small text-muted" id="colorHexLabel"></span>
|
|
||||||
</div>
|
|
||||||
{% if form.color.errors %}<div class="text-danger small mt-1">{{ form.color.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="form-check form-switch mt-2">
|
|
||||||
{{ form.is_active }}
|
|
||||||
<label class="form-check-label fw-semibold ms-2" for="{{ form.is_active.id_for_label }}">
|
|
||||||
{% translate "Zone active (affichée sur les cartes d'exploitation)" %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Entretien, Calendrier & Consignes -->
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header bg-light fw-bold">
|
|
||||||
<i class="bi bi-calendar-check me-2 text-success"></i>{% translate "Modalités d'entretien & Consignes" %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<label for="{{ form.target_frequency_days.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Fréquence cible (jours)" %}
|
|
||||||
</label>
|
|
||||||
<div class="input-group">
|
|
||||||
{{ form.target_frequency_days }}
|
|
||||||
<span class="input-group-text">{% translate "jours" %}</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-text small">{% translate "Intervalle indicatif entre passages" %}</div>
|
|
||||||
{% if form.target_frequency_days.errors %}<div class="text-danger small mt-1">{{ form.target_frequency_days.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-7">
|
|
||||||
<label for="{{ form.seasonality_notes.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Saisonnalité / Calendrier" %}
|
|
||||||
</label>
|
|
||||||
{{ form.seasonality_notes }}
|
|
||||||
<div class="form-text small">{% translate "Ex: Fauche en sept; interdiction taille mars-juillet" %}</div>
|
|
||||||
{% if form.seasonality_notes.errors %}<div class="text-danger small mt-1">{{ form.seasonality_notes.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<label for="{{ form.instructions.id_for_label }}" class="form-label fw-semibold text-warning-emphasis">
|
|
||||||
<i class="bi bi-exclamation-triangle me-1 text-warning"></i>{% translate "Consignes et points d'attention" %}
|
|
||||||
</label>
|
|
||||||
{{ form.instructions }}
|
|
||||||
<div class="form-text small">{% translate "Consignes cruciales : sécurité tramway, respect des zones humides, hauteur de coupe, etc." %}</div>
|
|
||||||
{% if form.instructions.errors %}<div class="text-danger small mt-1">{{ form.instructions.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<label for="{{ form.description.id_for_label }}" class="form-label fw-semibold">
|
|
||||||
{% translate "Description générale" %}
|
|
||||||
</label>
|
|
||||||
{{ form.description }}
|
|
||||||
{% if form.description.errors %}<div class="text-danger small mt-1">{{ form.description.errors.0 }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: Interactive Map Polygon Drawer -->
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="card shadow-sm mb-4 sticky-top" style="top: 20px;">
|
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<i class="bi bi-bounding-box-circles me-2 text-success"></i>
|
|
||||||
<span class="fw-bold">{% translate "Périmètre géographique" %}</span>
|
|
||||||
<small class="text-muted fw-normal">({% translate "polygone" %})</small>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-1">
|
|
||||||
<button type="button" id="polyPickerDrawBtn" class="btn btn-sm btn-outline-success">
|
|
||||||
<i class="bi bi-pentagon me-1"></i>{% translate "Dessiner" %}
|
|
||||||
</button>
|
|
||||||
<button type="button" id="polyPickerFinishBtn" class="btn btn-sm btn-success d-none">
|
|
||||||
<i class="bi bi-check-circle me-1"></i>{% translate "Terminer le polygone" %}
|
|
||||||
</button>
|
|
||||||
<button type="button" id="polyPickerClearBtn" class="btn btn-sm btn-outline-danger" title="{% translate 'Effacer le polygone' %}">
|
|
||||||
<i class="bi bi-x-circle"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0 position-relative">
|
|
||||||
<div id="zonePickerMap" style="height: 480px; width: 100%;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer bg-white border-top small text-muted d-flex justify-content-between align-items-center">
|
|
||||||
<span id="polyPickerHint">
|
|
||||||
{% translate "Cliquez sur « Dessiner » pour tracer les limites de la zone sur la carte." %}
|
|
||||||
</span>
|
|
||||||
<span id="polyStatusBadge" class="badge bg-secondary">
|
|
||||||
{% translate "Aucun tracé" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Form Submit Actions -->
|
|
||||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
|
||||||
{% if not is_create and zone %}
|
|
||||||
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="btn btn-outline-secondary">
|
|
||||||
{% translate "Annuler" %}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'assets:management_zones_index' %}" class="btn btn-outline-secondary">
|
|
||||||
{% translate "Annuler" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
<button type="submit" class="btn btn-success px-4">
|
|
||||||
<i class="bi bi-check-lg me-1"></i>
|
|
||||||
{% if is_create %}{% translate "Créer la zone de gestion" %}{% else %}{% translate "Enregistrer les modifications" %}{% endif %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
const mapDiv = document.getElementById("zonePickerMap");
|
|
||||||
const geomInput = document.getElementById("id_geom_geojson");
|
|
||||||
const thematicSelect = document.getElementById("id_thematic");
|
|
||||||
const zoneTypeSelect = document.getElementById("id_zone_type");
|
|
||||||
const colorInput = document.getElementById("id_color");
|
|
||||||
const colorHexLabel = document.getElementById("colorHexLabel");
|
|
||||||
|
|
||||||
if (colorInput && colorHexLabel) {
|
|
||||||
colorHexLabel.textContent = colorInput.value;
|
|
||||||
colorInput.addEventListener("input", () => {
|
|
||||||
colorHexLabel.textContent = colorInput.value;
|
|
||||||
if (map && map.getLayer("poly-preview-fill")) {
|
|
||||||
map.setPaintProperty("poly-preview-fill", "fill-color", colorInput.value);
|
|
||||||
map.setPaintProperty("poly-preview-line", "line-color", colorInput.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic zone types when thematic changes
|
|
||||||
if (thematicSelect && zoneTypeSelect) {
|
|
||||||
thematicSelect.addEventListener("change", async function () {
|
|
||||||
const thematicId = thematicSelect.value;
|
|
||||||
if (!thematicId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch("{% url 'assets:management_zone_types_ajax' %}?thematic_id=" + encodeURIComponent(thematicId));
|
|
||||||
if (resp.ok) {
|
|
||||||
const data = await resp.json();
|
|
||||||
const currentVal = zoneTypeSelect.value;
|
|
||||||
zoneTypeSelect.innerHTML = '<option value="">-- {% translate "Sélectionner un type" %} --</option>';
|
|
||||||
(data.types || []).forEach(t => {
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = t.id;
|
|
||||||
opt.textContent = t.name;
|
|
||||||
if (String(t.id) === String(currentVal)) opt.selected = true;
|
|
||||||
zoneTypeSelect.appendChild(opt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error updating zone types:", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interactive Map Polygon Drawer
|
|
||||||
if (!mapDiv || typeof maplibregl === "undefined") return;
|
|
||||||
|
|
||||||
let drawing = false;
|
|
||||||
let vertices = [];
|
|
||||||
let vertexMarkers = [];
|
|
||||||
let map = null;
|
|
||||||
|
|
||||||
const drawBtn = document.getElementById("polyPickerDrawBtn");
|
|
||||||
const finishBtn = document.getElementById("polyPickerFinishBtn");
|
|
||||||
const clearBtn = document.getElementById("polyPickerClearBtn");
|
|
||||||
const hintEl = document.getElementById("polyPickerHint");
|
|
||||||
const badgeEl = document.getElementById("polyStatusBadge");
|
|
||||||
|
|
||||||
map = new maplibregl.Map({
|
|
||||||
container: mapDiv,
|
|
||||||
style: window.MAP_STYLE_LIGHT,
|
|
||||||
center: window.MAP_DEFAULT_CENTER || [4.3572, 50.8477],
|
|
||||||
zoom: 12
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1. Fond de plan & Orthophotos (LayerSwitcherControl)
|
|
||||||
if (typeof LayerSwitcherControl !== 'undefined') {
|
|
||||||
const layerSwitcher = new LayerSwitcherControl({
|
|
||||||
layers: [{
|
|
||||||
id: 'basemap',
|
|
||||||
name: "{% translate 'Fonds de plan & Orthophotos' %}",
|
|
||||||
icon: 'satellite',
|
|
||||||
group: 'basemap',
|
|
||||||
beforeLayer: function (mapInstance) {
|
|
||||||
if (mapInstance && mapInstance.getLayer('poly-preview-fill')) {
|
|
||||||
return 'poly-preview-fill';
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
layer: {
|
|
||||||
type: 'raster',
|
|
||||||
paint: { 'raster-opacity': 1 }
|
|
||||||
},
|
|
||||||
basemaps: (window.MAP_BACKGROUNDS && window.MAP_BACKGROUNDS.length > 0) ? window.MAP_BACKGROUNDS : undefined,
|
|
||||||
sublayers: [
|
|
||||||
{
|
|
||||||
id: 'urbis',
|
|
||||||
name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)',
|
|
||||||
default: true,
|
|
||||||
source: {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: [
|
|
||||||
window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}'
|
|
||||||
],
|
|
||||||
tileSize: 256,
|
|
||||||
attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'vlaanderen',
|
|
||||||
name: 'Vlaanderen 2023',
|
|
||||||
source: {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: [
|
|
||||||
'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'
|
|
||||||
],
|
|
||||||
tileSize: 256,
|
|
||||||
attribution: '© Digitaal Vlaanderen - Orthofoto 2023'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
map.addControl(layerSwitcher, 'top-right');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Navigation Control (Zoom +/- & Pitch/Bearing)
|
|
||||||
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), 'top-right');
|
|
||||||
|
|
||||||
// 3. Outil de recherche d'adresse
|
|
||||||
if (typeof AddressSearchControl !== 'undefined') {
|
|
||||||
const searchCtrl = new AddressSearchControl({
|
|
||||||
autocompleteUrl: "{% url 'common:autocomplete_street' %}",
|
|
||||||
placeholder: "{% translate 'Rechercher une adresse…' %}"
|
|
||||||
});
|
|
||||||
map.addControl(searchCtrl, 'top-left');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColor() {
|
|
||||||
return colorInput ? colorInput.value : "#198754";
|
|
||||||
}
|
|
||||||
|
|
||||||
function initPolyLayers() {
|
|
||||||
if (!map.getSource("poly-preview")) {
|
|
||||||
map.addSource("poly-preview", {
|
|
||||||
type: "geojson",
|
|
||||||
data: { type: "Feature", geometry: { type: "Polygon", coordinates: [] } }
|
|
||||||
});
|
|
||||||
map.addLayer({
|
|
||||||
id: "poly-preview-fill",
|
|
||||||
type: "fill",
|
|
||||||
source: "poly-preview",
|
|
||||||
paint: {
|
|
||||||
"fill-color": getColor(),
|
|
||||||
"fill-opacity": 0.25
|
|
||||||
}
|
|
||||||
});
|
|
||||||
map.addLayer({
|
|
||||||
id: "poly-preview-line",
|
|
||||||
type: "line",
|
|
||||||
source: "poly-preview",
|
|
||||||
paint: {
|
|
||||||
"line-color": getColor(),
|
|
||||||
"line-width": 2.5
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearMarkers() {
|
|
||||||
vertexMarkers.forEach(m => m.remove());
|
|
||||||
vertexMarkers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function addVertexMarker(lng, lat, index) {
|
|
||||||
const el = document.createElement("div");
|
|
||||||
el.style.width = "10px";
|
|
||||||
el.style.height = "10px";
|
|
||||||
el.style.borderRadius = "50%";
|
|
||||||
el.style.backgroundColor = index === 0 ? "#dc3545" : "#198754";
|
|
||||||
el.style.border = "2px solid #ffffff";
|
|
||||||
el.style.boxShadow = "0 0 3px rgba(0,0,0,0.5)";
|
|
||||||
el.style.cursor = index === 0 && drawing && vertices.length >= 3 ? "pointer" : "default";
|
|
||||||
|
|
||||||
if (index === 0) {
|
|
||||||
el.title = "{% translate 'Cliquez pour clore le polygone' %}";
|
|
||||||
el.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (drawing && vertices.length >= 3) {
|
|
||||||
finishPolygon();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const marker = new maplibregl.Marker({ element: el }).setLngLat([lng, lat]).addTo(map);
|
|
||||||
vertexMarkers.push(marker);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPoly(coords) {
|
|
||||||
initPolyLayers();
|
|
||||||
let geom;
|
|
||||||
if (coords.length >= 3) {
|
|
||||||
geom = {
|
|
||||||
type: "Polygon",
|
|
||||||
coordinates: [coords.concat([coords[0]])]
|
|
||||||
};
|
|
||||||
} else if (coords.length === 2) {
|
|
||||||
geom = {
|
|
||||||
type: "LineString",
|
|
||||||
coordinates: coords
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
geom = { type: "Point", coordinates: coords[0] || [0, 0] };
|
|
||||||
}
|
|
||||||
map.getSource("poly-preview").setData({ type: "Feature", geometry: geom });
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishPolygon() {
|
|
||||||
if (vertices.length < 3) {
|
|
||||||
alert("{% translate 'Un polygone doit comporter au moins 3 sommets.' %}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
drawing = false;
|
|
||||||
const closed = vertices.concat([vertices[0]]);
|
|
||||||
const polyGeojson = {
|
|
||||||
type: "Polygon",
|
|
||||||
coordinates: [closed]
|
|
||||||
};
|
|
||||||
if (geomInput) {
|
|
||||||
geomInput.value = JSON.stringify(polyGeojson);
|
|
||||||
}
|
|
||||||
renderPoly(vertices);
|
|
||||||
updateUI(false);
|
|
||||||
hintEl.textContent = "{% translate 'Polygone enregistré. Cliquez sur « Dessiner » pour le refaire.' %}";
|
|
||||||
badgeEl.className = "badge bg-success";
|
|
||||||
badgeEl.textContent = `{% translate 'Polygone défini' %} (${vertices.length} {% translate 'sommets' %})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUI(isDrawing) {
|
|
||||||
drawBtn.classList.toggle("d-none", isDrawing);
|
|
||||||
finishBtn.classList.toggle("d-none", !isDrawing);
|
|
||||||
map.getCanvas().style.cursor = isDrawing ? "crosshair" : "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
function showExistingPolygon(geojsonStr) {
|
|
||||||
if (!geojsonStr) return;
|
|
||||||
try {
|
|
||||||
const geom = JSON.parse(geojsonStr);
|
|
||||||
let coords = null;
|
|
||||||
if (geom.type === "Polygon") {
|
|
||||||
coords = geom.coordinates[0];
|
|
||||||
} else if (geom.type === "MultiPolygon") {
|
|
||||||
coords = geom.coordinates[0][0];
|
|
||||||
}
|
|
||||||
if (!coords || coords.length === 0) return;
|
|
||||||
|
|
||||||
// Strip closing duplicate vertex for marker tracking
|
|
||||||
const rawVertices = (coords[0][0] === coords[coords.length - 1][0] && coords[0][1] === coords[coords.length - 1][1])
|
|
||||||
? coords.slice(0, coords.length - 1)
|
|
||||||
: coords;
|
|
||||||
|
|
||||||
vertices = rawVertices;
|
|
||||||
clearMarkers();
|
|
||||||
vertices.forEach((c, idx) => addVertexMarker(c[0], c[1], idx));
|
|
||||||
renderPoly(vertices);
|
|
||||||
|
|
||||||
// Fit bounds
|
|
||||||
const bounds = new maplibregl.LngLatBounds();
|
|
||||||
coords.forEach(c => bounds.extend(c));
|
|
||||||
map.fitBounds(bounds, { padding: 50, maxZoom: 16 });
|
|
||||||
|
|
||||||
badgeEl.className = "badge bg-success";
|
|
||||||
badgeEl.textContent = `{% translate 'Polygone existant' %} (${vertices.length} {% translate 'sommets' %})`;
|
|
||||||
hintEl.textContent = "{% translate 'Polygone actuel affiché. Cliquez sur « Dessiner » pour le modifier.' %}";
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Could not parse existing geometry:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map.on("load", function () {
|
|
||||||
initPolyLayers();
|
|
||||||
if (geomInput && geomInput.value) {
|
|
||||||
showExistingPolygon(geomInput.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("click", function (e) {
|
|
||||||
if (!drawing) return;
|
|
||||||
const { lng, lat } = e.lngLat;
|
|
||||||
vertices.push([lng, lat]);
|
|
||||||
addVertexMarker(lng, lat, vertices.length - 1);
|
|
||||||
renderPoly(vertices);
|
|
||||||
|
|
||||||
hintEl.textContent = `{% translate 'Sommets posés' %} : ${vertices.length}. {% translate 'Cliquez sur le premier sommet ou sur « Terminer » pour clore.' %}`;
|
|
||||||
badgeEl.className = "badge bg-warning text-dark";
|
|
||||||
badgeEl.textContent = `{% translate 'Tracé en cours' %} (${vertices.length})`;
|
|
||||||
});
|
|
||||||
|
|
||||||
drawBtn.addEventListener("click", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
drawing = true;
|
|
||||||
vertices = [];
|
|
||||||
clearMarkers();
|
|
||||||
if (geomInput) geomInput.value = "";
|
|
||||||
initPolyLayers();
|
|
||||||
map.getSource("poly-preview").setData({ type: "Feature", geometry: { type: "Polygon", coordinates: [] } });
|
|
||||||
updateUI(true);
|
|
||||||
hintEl.textContent = "{% translate 'Cliquez sur la carte pour poser chaque sommet.' %}";
|
|
||||||
badgeEl.className = "badge bg-warning text-dark";
|
|
||||||
badgeEl.textContent = "{% translate 'Tracé en cours' %}";
|
|
||||||
});
|
|
||||||
|
|
||||||
finishBtn.addEventListener("click", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
finishPolygon();
|
|
||||||
});
|
|
||||||
|
|
||||||
clearBtn.addEventListener("click", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
drawing = false;
|
|
||||||
vertices = [];
|
|
||||||
clearMarkers();
|
|
||||||
if (geomInput) geomInput.value = "";
|
|
||||||
initPolyLayers();
|
|
||||||
map.getSource("poly-preview").setData({ type: "Feature", geometry: { type: "Polygon", coordinates: [] } });
|
|
||||||
updateUI(false);
|
|
||||||
hintEl.textContent = "{% translate 'Périmètre effacé. Cliquez sur « Dessiner » pour recommencer.' %}";
|
|
||||||
badgeEl.className = "badge bg-secondary";
|
|
||||||
badgeEl.textContent = "{% translate 'Aucun tracé' %}";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock script %}
|
|
||||||
|
|
@ -1,340 +0,0 @@
|
||||||
{% 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 %}
|
|
||||||
|
|
@ -1,15 +1,6 @@
|
||||||
{% extends "assets/base_assets_index.html" %}
|
{% extends "assets/base_assets_index.html" %}
|
||||||
{% load static i18n %}
|
{% load static i18n %}
|
||||||
|
|
||||||
{% block tab_actions %}
|
|
||||||
<li class="nav-item ms-auto">
|
|
||||||
<a href="{% url 'assets:management_zones_index' %}?thematic=nature" class="btn btn-sm btn-outline-success d-inline-flex align-items-center gap-1 my-1 me-2" title="{% translate "Gérer les zones de gestion écologique et d'entretien" %}">
|
|
||||||
<i class="bi bi-diagram-3"></i>
|
|
||||||
<span>{% translate "Zones de gestion" %}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endblock tab_actions %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block locations_filter_form %}
|
{% block locations_filter_form %}
|
||||||
<form method="get" id="filter-form" class="row g-3 mb-4" style="width: 30vw;">
|
<form method="get" id="filter-form" class="row g-3 mb-4" style="width: 30vw;">
|
||||||
|
|
@ -64,16 +55,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
|
||||||
<label for="management_type" class="form-label">{% translate "Gestionnaire / Compétence" %}</label>
|
|
||||||
<select name="management_type" id="management_type" class="form-select" multiple>
|
|
||||||
{% for key, value in management_type_choices %}
|
|
||||||
<option value="{{ key }}" {% if key in selected_management_types %}selected{% endif %}>
|
|
||||||
{{ value }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 d-flex justify-content-between mt-4" style="width: 200px;">
|
<div class="col-12 d-flex justify-content-between mt-4" style="width: 200px;">
|
||||||
<button type="button" id="apply-assets-filters" class="btn btn-sm btn-primary">{% translate "Filtrer" %}</button>
|
<button type="button" id="apply-assets-filters" class="btn btn-sm btn-primary">{% translate "Filtrer" %}</button>
|
||||||
<button type="button" id="reset-assets-filters" class="btn btn-sm btn-secondary">{% translate "Réinitialiser" %}</button>
|
<button type="button" id="reset-assets-filters" class="btn btn-sm btn-secondary">{% translate "Réinitialiser" %}</button>
|
||||||
|
|
@ -96,15 +77,12 @@
|
||||||
{ field: 'highway', title: '{% translate "Ring/Autoroute" %}', align: 'center' }
|
{ field: 'highway', title: '{% translate "Ring/Autoroute" %}', align: 'center' }
|
||||||
];
|
];
|
||||||
|
|
||||||
var assets_property_fields = ['code', 'name', 'managing_authority', 'model', 'scientific_name', 'area'];
|
var assets_property_fields = ['category', 'model', 'area'];
|
||||||
var assets_columns = [
|
var assets_columns = [
|
||||||
{ field: 'code', title: '{% translate "Code" %}' },
|
{ field: 'category', title: '{% translate "Catégorie" %}' },
|
||||||
{ field: 'name', title: '{% translate "Nom" %}' },
|
|
||||||
{ field: 'managing_authority', title: '{% translate "Gestionnaire" %}' },
|
|
||||||
{ field: 'model', title: '{% translate "Modèle" %}' },
|
{ field: 'model', title: '{% translate "Modèle" %}' },
|
||||||
{ field: 'scientific_name', title: '{% translate "Botanique" %}' },
|
|
||||||
{ field: 'area', title: '{% translate "Superficie" %}' }
|
{ field: 'area', title: '{% translate "Superficie" %}' }
|
||||||
];
|
]
|
||||||
|
|
||||||
|
|
||||||
const locations_layer_def = {
|
const locations_layer_def = {
|
||||||
|
|
@ -174,25 +152,16 @@
|
||||||
16, 5,
|
16, 5,
|
||||||
20, 8
|
20, 8
|
||||||
],
|
],
|
||||||
'circle-color': [
|
'circle-color': ['coalesce', ['get', 'color'], '#0277BD'],
|
||||||
'case',
|
|
||||||
['==', ['get', 'management_type'], 'regional'], '#1E3A8A',
|
|
||||||
['boolean', ['get', 'is_remarkable'], false], '#F59E0B',
|
|
||||||
['coalesce', ['get', 'color'], '#15803D']
|
|
||||||
],
|
|
||||||
'circle-stroke-width': [
|
'circle-stroke-width': [
|
||||||
'case',
|
'case',
|
||||||
['boolean', ['feature-state', 'highlighted'], false], 2.5,
|
['boolean', ['feature-state', 'highlighted'], false], 2,
|
||||||
['==', ['get', 'management_type'], 'regional'], 1.5,
|
|
||||||
['boolean', ['get', 'is_remarkable'], false], 2,
|
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
'circle-stroke-color': [
|
'circle-stroke-color': [
|
||||||
'case',
|
'case',
|
||||||
['boolean', ['feature-state', 'highlighted'], false], '#EF4444',
|
['boolean', ['feature-state', 'highlighted'], false], '#ff0000',
|
||||||
['==', ['get', 'management_type'], 'regional'], '#93C5FD',
|
'#000000'
|
||||||
['boolean', ['get', 'is_remarkable'], false], '#FEF08A',
|
|
||||||
'#064E3B'
|
|
||||||
],
|
],
|
||||||
'circle-opacity': 0.8
|
'circle-opacity': 0.8
|
||||||
},
|
},
|
||||||
|
|
@ -252,36 +221,5 @@
|
||||||
{% endblock const_script %}
|
{% endblock const_script %}
|
||||||
|
|
||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
function setupNatureOverlays() {
|
|
||||||
if (typeof initContractsMapOverlay === 'function' && window.map) {
|
|
||||||
initContractsMapOverlay({
|
|
||||||
map: window.map,
|
|
||||||
layerSwitcher: window.layerSwitcher,
|
|
||||||
thematic: 'nature',
|
|
||||||
geojsonUrl: "{% url 'contracts:contracts_geojson' %}",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (typeof initManagementZonesMapOverlay === 'function' && window.map) {
|
|
||||||
initManagementZonesMapOverlay({
|
|
||||||
map: window.map,
|
|
||||||
layerSwitcher: window.layerSwitcher,
|
|
||||||
thematic: 'nature',
|
|
||||||
geojsonUrl: "{% url 'assets:management_zones_geojson' %}",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.map && window.map.loaded && window.map.loaded()) {
|
|
||||||
setupNatureOverlays();
|
|
||||||
} else if (window.maploader) {
|
|
||||||
window.maploader.addEventListener('mapLoaded', () => {
|
|
||||||
setTimeout(setupNatureOverlays, 100);
|
|
||||||
});
|
|
||||||
} else if (window.map) {
|
|
||||||
window.map.on('load', setupNatureOverlays);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock extra_script %}
|
{% endblock extra_script %}
|
||||||
|
|
@ -2,704 +2,116 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block extra_head %}
|
|
||||||
<style>
|
|
||||||
.nature-kpi-card {
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
.nature-kpi-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
.nature-kpi-icon {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
min-width: 44px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
.nature-tab-content {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-top: none;
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
#map {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 230px);
|
|
||||||
min-height: 520px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
.map-card-wrapper {
|
|
||||||
position: sticky;
|
|
||||||
top: 1rem;
|
|
||||||
}
|
|
||||||
.map-layer-pill {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock extra_head %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-3">
|
<div class="row mt-2" style="height: calc(100vh - 200px);">
|
||||||
|
<div class="col-md-8" style="max-height: calc(100vh - 200px); overflow: auto;">
|
||||||
<!-- Breadcrumb & Navigation -->
|
<h3 class="h3">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
<i class="bi bi-suit-club"></i><span class="mx-2">{{ section.code}} - {{ section.name_fr}}</span>
|
||||||
<nav aria-label="breadcrumb">
|
</h3>
|
||||||
<ol class="breadcrumb mb-0">
|
<hr>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'assets:index' %}?thematic=nature" class="text-decoration-none">{% translate "Nature" %}</a></li>
|
<div class="row mb-4">
|
||||||
<li class="breadcrumb-item active fw-semibold" aria-current="page">{{ section.code }}</li>
|
<div class="col-md-6">
|
||||||
</ol>
|
<label class="mx-2">{% translate "Zone:" %}</label><i class="bi bi-circle-fill status_{{ surface.status }}"></i> {{ section.zone|default:"" }}<br>
|
||||||
</nav>
|
<label class="mx-2">{% translate "Autoroute:" %}</label>{{ section.is_highway|default:"" }}<br>
|
||||||
<div class="d-flex gap-2">
|
</div>
|
||||||
<a href="{% url 'assets:index' %}?thematic=nature" class="btn btn-sm btn-outline-secondary">
|
<div class="col-md-6">
|
||||||
<i class="bi bi-arrow-left me-1"></i>{% translate "Retour à la carte" %}
|
{% if section.lon %}
|
||||||
</a>
|
<a href="https://www.google.com/maps?cbll={{ section.lat|floatformat:'6u'}},{{ section.lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid=" target="_blank" style="text-decoration: none; font-size: 16px; display: flex; align-items: center;">
|
||||||
{% if section.lon and section.lat %}
|
<img src="{% static 'common/google-street-view-logo.png' %}" alt="{% translate 'Street View' %}" height="20" style="margin-right: 8px;">
|
||||||
<a href="https://www.google.com/maps?cbll={{ section.lat|floatformat:'6u'}},{{ section.lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid="
|
|
||||||
target="_blank" class="btn btn-sm btn-outline-primary" style="text-decoration: none;">
|
|
||||||
<img src="{% static 'common/google-street-view-logo.png' %}" alt="{% translate 'Street View' %}" height="18" class="me-1">
|
|
||||||
{% translate "View in Google Street View" %}
|
{% translate "View in Google Street View" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="mx-2">{% translate "Nombre d'arbres:" %}</label><br>
|
||||||
|
<label class="mx-2">{% translate "Superficie massif arboré:" %}</label><br>
|
||||||
|
<label class="mx-2">{% translate "Superficie pré fleuri:" %}</label><br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Header Card -->
|
<!-- Documents -->
|
||||||
<div class="card shadow-sm mb-4 border-0" style="background: linear-gradient(to right, #f8fafc, #ffffff); border-left: 5px solid #16a34a !important;">
|
<div class="card mb-4">
|
||||||
<div class="card-body p-3 p-md-4">
|
<div class="card-header">
|
||||||
<div class="row align-items-center g-3">
|
<h4>{% translate "Documents" %}</h4>
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<div class="rounded-circle bg-success-subtle p-2 d-flex align-items-center justify-content-center text-success" style="width: 52px; height: 52px; min-width: 52px;">
|
|
||||||
<i class="bi bi-tree-fill" style="font-size: 1.8rem;"></i>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
<div class="row">
|
||||||
<h1 class="h4 mb-0 fw-bold text-dark">
|
{% for document in section.documents.all %}
|
||||||
{{ section.code }} <span class="text-muted fw-normal">— {{ section.get_name }}</span>
|
<div class="col-md-2 mb-3">
|
||||||
</h1>
|
<a href="{{ document.file.url }}" target="_blank">
|
||||||
</div>
|
<div class="img-thumbnail" style="width: 100%; height: 100px; overflow: hidden;">
|
||||||
<div class="d-flex align-items-center gap-2 mt-2 flex-wrap text-muted small">
|
{% if document.thumbnail %}
|
||||||
{% if section.municipality %}
|
<img src="{{ document.thumbnail.url }}" alt="{{ document.file.name }}" style="width: 100%; height: 100%; object-fit: cover; object-position: center;">
|
||||||
<span><i class="bi bi-geo-alt-fill text-danger me-1"></i>{{ section.municipality }}</span>
|
|
||||||
<span>•</span>
|
|
||||||
{% endif %}
|
|
||||||
<span>
|
|
||||||
<strong>{% translate "Zone:" %}</strong>
|
|
||||||
{% if section.zone %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary border">Zone {{ section.zone }}</span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">{% translate "Non définie" %}</span>
|
<img src="{{ document.file.url }}" alt="{{ document.file.name }}" style="width: 100%; height: 100%; object-fit: cover; object-position: center;">
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>
|
|
||||||
<strong>{% translate "Autoroute:" %}</strong>
|
|
||||||
{% if section.is_highway %}
|
|
||||||
<span class="badge bg-primary-subtle text-primary border">{% translate "Oui" %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-light text-secondary border">{% translate "Non" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{% if section.category %}
|
|
||||||
<span>•</span>
|
|
||||||
<span><i class="bi bi-tag me-1"></i>{{ section.category.get_name }}</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if section.parent_location or section.child_locations.exists %}
|
|
||||||
<div class="mt-2 pt-2 border-top d-flex gap-3 flex-wrap small text-muted">
|
|
||||||
{% if section.parent_location %}
|
|
||||||
<div>
|
|
||||||
<i class="bi bi-diagram-2 me-1"></i>{% translate "Section parente :" %}
|
|
||||||
<a href="{{ section.parent_location.get_absolute_url }}" class="fw-semibold text-decoration-none">
|
|
||||||
{{ section.parent_location.code }} - {{ section.parent_location.get_name }}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% if section.child_locations.exists %}
|
|
||||||
<div>
|
|
||||||
<i class="bi bi-diagram-3 me-1"></i>{% translate "Sous-sections :" %}
|
|
||||||
{% for child in section.child_locations.all %}
|
|
||||||
<a href="{{ child.get_absolute_url }}" class="badge bg-light text-dark border text-decoration-none me-1">
|
|
||||||
{{ child.code }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-4 text-lg-end">
|
|
||||||
<div class="d-inline-flex gap-2 flex-wrap justify-content-lg-end">
|
|
||||||
<div class="badge bg-light text-dark border p-2 text-start">
|
|
||||||
<span class="d-block text-muted" style="font-size: 0.72rem;">{% translate "Total équipements" %}</span>
|
|
||||||
<span class="fw-bold fs-6">{{ total_assets_count }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="badge bg-light text-dark border p-2 text-start">
|
|
||||||
<span class="d-block text-muted" style="font-size: 0.72rem;">{% translate "Interventions" %}</span>
|
|
||||||
<span class="fw-bold fs-6 text-primary">{{ interventions|length }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- KPI Metric Cards Grid -->
|
|
||||||
<div class="row g-3 mb-4">
|
|
||||||
<!-- 1. Nombre d'arbres -->
|
|
||||||
<div class="col-sm-6 col-xl-3">
|
|
||||||
<div class="card h-100 nature-kpi-card shadow-sm p-3">
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<div class="nature-kpi-icon bg-success-subtle text-success">
|
|
||||||
<i class="bi bi-tree-fill"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-muted small fw-semibold">{% translate "Nombre d'arbres:" %}</div>
|
|
||||||
<div class="fs-4 fw-bold text-dark">{{ trees_count }}</div>
|
|
||||||
<div class="text-muted" style="font-size: 0.75rem;">
|
|
||||||
{{ trees_active_count }} {% translate "actifs" %}
|
|
||||||
{% if trees_remarkable_count %}
|
|
||||||
• <span class="text-warning fw-semibold"><i class="bi bi-star-fill text-warning"></i> {{ trees_remarkable_count }} {% translate "remarquables" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 2. Superficie totale surfaces vertes -->
|
|
||||||
<div class="col-sm-6 col-xl-3">
|
|
||||||
<div class="card h-100 nature-kpi-card shadow-sm p-3">
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<div class="nature-kpi-icon bg-primary-subtle text-primary">
|
|
||||||
<i class="bi bi-grid-fill"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-muted small fw-semibold">{% translate "Surfaces végétales:" %}</div>
|
|
||||||
<div class="fs-4 fw-bold text-dark">{{ total_green_surface|floatformat:1 }} <span class="fs-6 fw-normal text-muted">m²</span></div>
|
|
||||||
<div class="text-muted" style="font-size: 0.75rem;">
|
|
||||||
{{ green_surfaces_count }} {% translate "surfaces au total" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 3. Superficie massif arboré -->
|
|
||||||
<div class="col-sm-6 col-xl-3">
|
|
||||||
<div class="card h-100 nature-kpi-card shadow-sm p-3">
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<div class="nature-kpi-icon bg-warning-subtle text-warning-emphasis">
|
|
||||||
<i class="bi bi-flower1"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-muted small fw-semibold">{% translate "Superficie massif arboré:" %}</div>
|
|
||||||
<div class="fs-4 fw-bold text-dark">{{ surface_massif_arbore|floatformat:1 }} <span class="fs-6 fw-normal text-muted">m²</span></div>
|
|
||||||
<div class="text-muted" style="font-size: 0.75rem;">
|
|
||||||
{{ massif_arbore_count }} {% translate "massifs arborés" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 4. Superficie pré fleuri -->
|
|
||||||
<div class="col-sm-6 col-xl-3">
|
|
||||||
<div class="card h-100 nature-kpi-card shadow-sm p-3">
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<div class="nature-kpi-icon bg-info-subtle text-info-emphasis">
|
|
||||||
<i class="bi bi-flower2"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-muted small fw-semibold">{% translate "Superficie pré fleuri:" %}</div>
|
|
||||||
<div class="fs-4 fw-bold text-dark">{{ surface_pre_fleuri|floatformat:1 }} <span class="fs-6 fw-normal text-muted">m²</span></div>
|
|
||||||
<div class="text-muted" style="font-size: 0.75rem;">
|
|
||||||
{{ pre_fleuri_count }} {% translate "prés fleuris" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Typology breakdown (if available) -->
|
|
||||||
{% if surfaces_by_type %}
|
|
||||||
<div class="card shadow-sm mb-4 border-0">
|
|
||||||
<div class="card-body p-3">
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
||||||
<span class="small fw-bold text-secondary text-uppercase"><i class="bi bi-pie-chart-fill me-1"></i>{% translate "Répartition des surfaces végétales" %}</span>
|
|
||||||
<span class="small text-muted">{{ total_green_surface|floatformat:1 }} m²</span>
|
|
||||||
</div>
|
|
||||||
<!-- Progress bar -->
|
|
||||||
<div class="progress mb-2" style="height: 10px; border-radius: 6px;">
|
|
||||||
{% for st in surfaces_by_type %}
|
|
||||||
<div class="progress-bar" role="progressbar"
|
|
||||||
style="width: {{ st.percent }}%; background-color: {{ st.color }};"
|
|
||||||
title="{{ st.name }}: {{ st.total_area|floatformat:1 }} m² ({{ st.percent }}%)"
|
|
||||||
aria-valuenow="{{ st.percent }}" aria-valuemin="0" aria-valuemax="100"></div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<!-- Badges -->
|
|
||||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
|
||||||
{% for st in surfaces_by_type %}
|
|
||||||
<span class="badge bg-light text-dark border d-inline-flex align-items-center gap-1 py-1 px-2" style="font-size: 0.8rem;">
|
|
||||||
<span class="d-inline-block rounded-circle" style="width: 8px; height: 8px; background-color: {{ st.color }};"></span>
|
|
||||||
<span class="fw-semibold">{{ st.name }}</span>:
|
|
||||||
<span class="text-muted">{{ st.total_area|floatformat:1 }} m² ({{ st.count }})</span>
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Main Content Layout (Split View: Tabs/Tables on Left, Interactive Map on Right) -->
|
<!-- Interventions -->
|
||||||
<div class="row g-4">
|
<div class="card mb-4">
|
||||||
<!-- Left Column: Tabs & Tables -->
|
<div class="card-header">
|
||||||
<div class="col-lg-7 col-xl-7">
|
<h4>{% translate "Interventions" %}</h4>
|
||||||
<!-- Tabs Navigation -->
|
|
||||||
<ul class="nav nav-tabs" id="natureLocationTabs" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link active fw-semibold" id="surfaces-tab" data-bs-toggle="tab" data-bs-target="#surfaces-pane" type="button" role="tab">
|
|
||||||
<i class="bi bi-grid-fill text-success me-1"></i>{% translate "Surfaces vertes" %}
|
|
||||||
<span class="badge bg-success-subtle text-success ms-1">{{ green_surfaces_count }}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link fw-semibold" id="trees-tab" data-bs-toggle="tab" data-bs-target="#trees-pane" type="button" role="tab">
|
|
||||||
<i class="bi bi-tree-fill text-success me-1"></i>{% translate "Arbres" %}
|
|
||||||
<span class="badge bg-success-subtle text-success ms-1">{{ trees_count }}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link fw-semibold" id="equipments-tab" data-bs-toggle="tab" data-bs-target="#equipments-pane" type="button" role="tab">
|
|
||||||
<i class="bi bi-flower3 text-info me-1"></i>{% translate "Arbustes & Bancs" %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary ms-1">{{ shrubs_count|add:benches_count }}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link fw-semibold" id="interventions-tab" data-bs-toggle="tab" data-bs-target="#interventions-pane" type="button" role="tab">
|
|
||||||
<i class="bi bi-tools text-primary me-1"></i>{% translate "Interventions" %}
|
|
||||||
<span class="badge bg-primary-subtle text-primary ms-1">{{ interventions|length }}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link fw-semibold" id="documents-tab" data-bs-toggle="tab" data-bs-target="#documents-pane" type="button" role="tab">
|
|
||||||
<i class="bi bi-files text-secondary me-1"></i>{% translate "Documents" %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary ms-1">{{ documents|length }}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- Tabs Content -->
|
|
||||||
<div class="tab-content nature-tab-content shadow-sm" id="natureLocationTabsContent">
|
|
||||||
|
|
||||||
<!-- TAB 1: Surfaces végétalisées -->
|
|
||||||
<div class="tab-pane fade show active" id="surfaces-pane" role="tabpanel" aria-labelledby="surfaces-tab">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover align-middle mb-0" style="font-size: 0.9rem;">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Code" %}</th>
|
|
||||||
<th>{% translate "Typologie / Modèle" %}</th>
|
|
||||||
<th class="text-end">{% translate "Superficie" %}</th>
|
|
||||||
<th>{% translate "Pente / Tonte" %}</th>
|
|
||||||
<th>{% translate "Bulbes" %}</th>
|
|
||||||
<th>{% translate "Statut" %}</th>
|
|
||||||
<th class="text-end">{% translate "Actions" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for gs in green_surfaces %}
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">
|
|
||||||
<a href="{% url 'assets:nature_assets_detail' 'naturegreensurface' gs.id %}" class="text-decoration-none">
|
|
||||||
{{ gs.code }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="d-inline-block rounded-circle me-1" style="width: 8px; height: 8px; background-color: {{ gs.model.color|default:'#4caf50' }};"></span>
|
|
||||||
{{ gs.model.get_name|default:"-" }}
|
|
||||||
</td>
|
|
||||||
<td class="text-end fw-semibold">
|
|
||||||
{% if gs.surface_area %}{{ gs.surface_area|floatformat:1 }} m²{% else %}<span class="text-muted">-</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="small text-muted">
|
|
||||||
{% if gs.slope %}{{ gs.slope }}%{% else %}-{% endif %} /
|
|
||||||
{% if gs.mower_frenquency %}{{ gs.mower_frenquency }}x/an{% else %}-{% endif %}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if gs.bulb_types %}
|
|
||||||
<span class="badge bg-info-subtle text-info border">{{ gs.bulb_types|length }} bulbe(s)</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted small">-</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge {% if gs.status == 'active' %}bg-success{% else %}bg-secondary{% endif %}">
|
|
||||||
{{ gs.get_status_display }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<div class="btn-group btn-group-sm">
|
|
||||||
{% if gs.lon and gs.lat %}
|
|
||||||
<button type="button" class="btn btn-outline-success" onclick="zoomToAsset({{ gs.lon|stringformat:'f' }}, {{ gs.lat|stringformat:'f' }}, '{{ gs.code }}')" title="{% translate 'Centrer sur la carte' %}">
|
|
||||||
<i class="bi bi-geo-alt"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'assets:nature_assets_detail' 'naturegreensurface' gs.id %}" class="btn btn-outline-primary" title="{% translate 'Détail' %}">
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="card-body">
|
||||||
</tr>
|
<table id="interventions-table" class="table"
|
||||||
{% empty %}
|
data-height="550"
|
||||||
<tr>
|
|
||||||
<td colspan="7" class="text-center text-muted py-4">
|
|
||||||
<i class="bi bi-grid mb-2 d-block fs-3 text-muted"></i>
|
|
||||||
{% translate "Aucune surface végétalisée enregistrée sur cette voirie." %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TAB 2: Arbres -->
|
|
||||||
<div class="tab-pane fade" id="trees-pane" role="tabpanel" aria-labelledby="trees-tab">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover align-middle mb-0" style="font-size: 0.9rem;">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Code" %}</th>
|
|
||||||
<th>{% translate "Essence / Modèle" %}</th>
|
|
||||||
<th>{% translate "Stade" %}</th>
|
|
||||||
<th>{% translate "Vitalité" %}</th>
|
|
||||||
<th>{% translate "Risque" %}</th>
|
|
||||||
<th>{% translate "Statut" %}</th>
|
|
||||||
<th class="text-end">{% translate "Actions" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for tree in trees %}
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">
|
|
||||||
<a href="{% url 'assets:nature_assets_detail' 'naturetree' tree.id %}" class="text-decoration-none">
|
|
||||||
{{ tree.code }}
|
|
||||||
</a>
|
|
||||||
{% if tree.is_remarkable %}
|
|
||||||
<i class="bi bi-star-fill text-warning ms-1" title="{% translate 'Arbre remarquable' %}"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>{{ tree.model.get_name|default:"-" }}</div>
|
|
||||||
{% if tree.model.scientific_name %}
|
|
||||||
<div class="small text-muted fst-italic">{{ tree.model.scientific_name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="small">{{ tree.get_development_stage_display|default:"-" }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if tree.vitality == 'strong' %}
|
|
||||||
<span class="badge bg-success-subtle text-success border">{% translate "Forte" %}</span>
|
|
||||||
{% elif tree.vitality == 'normal' %}
|
|
||||||
<span class="badge bg-info-subtle text-info border">{% translate "Normale" %}</span>
|
|
||||||
{% elif tree.vitality == 'weak' %}
|
|
||||||
<span class="badge bg-warning-subtle text-warning-emphasis border">{% translate "Faible" %}</span>
|
|
||||||
{% elif tree.vitality == 'declining' or tree.vitality == 'dead' %}
|
|
||||||
<span class="badge bg-danger">{{ tree.get_vitality_display }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted small">-</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if tree.risk_level == 'high' or tree.risk_level == 'imminent_danger' %}
|
|
||||||
<span class="badge bg-danger">{{ tree.get_risk_level_display }}</span>
|
|
||||||
{% elif tree.risk_level == 'moderate' %}
|
|
||||||
<span class="badge bg-warning text-dark">{{ tree.get_risk_level_display }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="small text-muted">{{ tree.get_risk_level_display|default:"Faible" }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge {% if tree.status == 'active' %}bg-success{% else %}bg-secondary{% endif %}">
|
|
||||||
{{ tree.get_status_display }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<div class="btn-group btn-group-sm">
|
|
||||||
{% if tree.lon and tree.lat %}
|
|
||||||
<button type="button" class="btn btn-outline-success" onclick="zoomToAsset({{ tree.lon|stringformat:'f' }}, {{ tree.lat|stringformat:'f' }}, '{{ tree.code }}')" title="{% translate 'Centrer sur la carte' %}">
|
|
||||||
<i class="bi bi-geo-alt"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'assets:nature_assets_detail' 'naturetree' tree.id %}" class="btn btn-outline-primary" title="{% translate 'Détail' %}">
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="7" class="text-center text-muted py-4">
|
|
||||||
<i class="bi bi-tree mb-2 d-block fs-3 text-muted"></i>
|
|
||||||
{% translate "Aucun arbre enregistré sur cette voirie." %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TAB 3: Arbustes & Bancs -->
|
|
||||||
<div class="tab-pane fade" id="equipments-pane" role="tabpanel" aria-labelledby="equipments-tab">
|
|
||||||
<!-- Arbustes -->
|
|
||||||
<h6 class="fw-bold mb-2 text-dark"><i class="bi bi-flower3 text-success me-1"></i>{% translate "Arbustes" %} ({{ shrubs_count }})</h6>
|
|
||||||
<div class="table-responsive mb-4">
|
|
||||||
<table class="table table-hover align-middle mb-0" style="font-size: 0.9rem;">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Code" %}</th>
|
|
||||||
<th>{% translate "Modèle" %}</th>
|
|
||||||
<th>{% translate "Hauteur" %}</th>
|
|
||||||
<th>{% translate "Taillé" %}</th>
|
|
||||||
<th>{% translate "Statut" %}</th>
|
|
||||||
<th class="text-end">{% translate "Actions" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for shrub in shrubs %}
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">
|
|
||||||
<a href="{% url 'assets:nature_assets_detail' 'natureshrub' shrub.id %}" class="text-decoration-none">
|
|
||||||
{{ shrub.code }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ shrub.model.get_name|default:"-" }}</td>
|
|
||||||
<td>{% if shrub.height %}{{ shrub.height }} m{% else %}-{% endif %}</td>
|
|
||||||
<td>
|
|
||||||
{% if shrub.trimmed %}<span class="badge bg-success-subtle text-success">{% translate "Oui" %}</span>{% else %}<span class="badge bg-light text-secondary">{% translate "Non" %}</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td><span class="badge bg-secondary">{{ shrub.get_status_display }}</span></td>
|
|
||||||
<td class="text-end">
|
|
||||||
<div class="btn-group btn-group-sm">
|
|
||||||
{% if shrub.lon and shrub.lat %}
|
|
||||||
<button type="button" class="btn btn-outline-success" onclick="zoomToAsset({{ shrub.lon|stringformat:'f' }}, {{ shrub.lat|stringformat:'f' }}, '{{ shrub.code }}')">
|
|
||||||
<i class="bi bi-geo-alt"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'assets:nature_assets_detail' 'natureshrub' shrub.id %}" class="btn btn-outline-primary">
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="6" class="text-center text-muted py-2 small">{% translate "Aucun arbuste répertorié." %}</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bancs / Mobilier -->
|
|
||||||
<h6 class="fw-bold mb-2 text-dark"><i class="bi bi-inbox text-warning me-1"></i>{% translate "Mobilier & Bancs" %} ({{ benches_count }})</h6>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover align-middle mb-0" style="font-size: 0.9rem;">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Code" %}</th>
|
|
||||||
<th>{% translate "Modèle" %}</th>
|
|
||||||
<th>{% translate "Matériau" %}</th>
|
|
||||||
<th>{% translate "Dimensions" %}</th>
|
|
||||||
<th>{% translate "Statut" %}</th>
|
|
||||||
<th class="text-end">{% translate "Actions" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for bench in benches %}
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">
|
|
||||||
<a href="{% url 'assets:nature_assets_detail' 'naturebench' bench.id %}" class="text-decoration-none">
|
|
||||||
{{ bench.code }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ bench.model.get_name|default:"-" }}</td>
|
|
||||||
<td>{{ bench.material|default:"-" }}</td>
|
|
||||||
<td>
|
|
||||||
{% if bench.length or bench.width or bench.height %}
|
|
||||||
{{ bench.length|default:"-" }} x {{ bench.width|default:"-" }} x {{ bench.height|default:"-" }} m
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td><span class="badge bg-secondary">{{ bench.get_status_display }}</span></td>
|
|
||||||
<td class="text-end">
|
|
||||||
<div class="btn-group btn-group-sm">
|
|
||||||
{% if bench.lon and bench.lat %}
|
|
||||||
<button type="button" class="btn btn-outline-success" onclick="zoomToAsset({{ bench.lon|stringformat:'f' }}, {{ bench.lat|stringformat:'f' }}, '{{ bench.code }}')">
|
|
||||||
<i class="bi bi-geo-alt"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'assets:nature_assets_detail' 'naturebench' bench.id %}" class="btn btn-outline-primary">
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="6" class="text-center text-muted py-2 small">{% translate "Aucun banc ou mobilier répertorié." %}</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TAB 4: Interventions -->
|
|
||||||
<div class="tab-pane fade" id="interventions-pane" role="tabpanel" aria-labelledby="interventions-tab">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table id="interventions-table" class="table table-hover align-middle mb-0"
|
|
||||||
data-pagination="true"
|
data-pagination="true"
|
||||||
data-page-size="25">
|
data-page-size="25"
|
||||||
<thead class="table-light">
|
data-click-to-select="true" >
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-field="actions" class="text-center" style="width: 50px;"></th>
|
<th data-field="actions" data-width="100"></th>
|
||||||
<th data-field="code">{% translate "Code" %}</th>
|
<th data-field="code">{% translate "Code" %}</th>
|
||||||
<th data-field="title">{% translate "Titre" %}</th>
|
<th data-field="title">{% translate "Titre" %}</th>
|
||||||
<th data-field="begin_date">{% translate "Début" %}</th>
|
<th data-field="begin_date">{% translate "Début" %}</th>
|
||||||
<th data-field="status">{% translate "Statut" %}</th>
|
<th data-field="statut">{% translate "Statut" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for interv in interventions %}
|
{% for interv in interventions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<a href="{% url 'interventions:interventions_index' %}{{ interv.id }}" class="btn btn-sm btn-light detail-btn" title="{% translate 'Détail' %}">
|
<a href="{% url 'interventions:interventions_index' %}{{ interv.id }}" type="button" class="btn btn-sm btn-light detail-btn" data-id="${row.id}" data-bs-toggle="tooltip" title="{% translate 'Détail' %}">
|
||||||
<i class="bi bi-info-circle"></i>
|
<i class="bi bi-info-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="fw-bold">{{ interv.code }}</td>
|
<td>{{ interv.code }}</td>
|
||||||
<td>{{ interv.title }}</td>
|
<td>{{ interv.title }}</td>
|
||||||
<td>{{ interv.begin_time|date:"d-m-Y H:i"|default:"-" }}</td>
|
<td>{{ interv.begin_time|date:"d-m-Y H:i" }}</td>
|
||||||
<td>
|
<td>{{ interv.get_status_display }}</td>
|
||||||
<span class="badge bg-secondary">{{ interv.get_status_display }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="text-center text-muted py-4">
|
|
||||||
<i class="bi bi-tools mb-2 d-block fs-3 text-muted"></i>
|
|
||||||
{% translate "Aucune intervention enregistrée sur cette localisation ou ses aménagements." %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TAB 5: Documents -->
|
|
||||||
<div class="tab-pane fade" id="documents-pane" role="tabpanel" aria-labelledby="documents-tab">
|
|
||||||
{% if documents %}
|
|
||||||
<div class="row g-3">
|
|
||||||
{% for document in documents %}
|
|
||||||
<div class="col-6 col-sm-4 col-md-3">
|
|
||||||
<div class="card h-100 shadow-sm border p-2 text-center">
|
|
||||||
<a href="{{ document.url }}" target="_blank" class="text-decoration-none text-dark">
|
|
||||||
<div class="img-thumbnail d-flex align-items-center justify-content-center mx-auto mb-2" style="width: 100%; height: 90px; overflow: hidden; background: #f8fafc;">
|
|
||||||
{% if document.thumbnail_url %}
|
|
||||||
<img src="{{ document.thumbnail_url }}" alt="{{ document.title }}" style="max-height: 100%; max-width: 100%; object-fit: contain;">
|
|
||||||
{% else %}
|
|
||||||
<i class="bi bi-file-earmark-text fs-2 text-secondary"></i>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="fw-semibold small text-truncate" title="{{ document.title }}">{{ document.title }}</div>
|
|
||||||
{% if document.tag %}
|
|
||||||
<span class="badge bg-primary-subtle text-primary border my-1" style="font-size: 0.7rem;">{{ document.tag }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if document.date %}
|
|
||||||
<div class="text-muted" style="font-size: 0.7rem;">{{ document.date|date:"d/m/Y" }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center text-muted py-5">
|
|
||||||
<i class="bi bi-folder2-open mb-2 d-block fs-2 text-muted"></i>
|
|
||||||
{% translate "Aucun document associé à cette localisation." %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div class="col-md-4">
|
||||||
</div>
|
<div id="map">
|
||||||
|
|
||||||
<!-- Right Column: Interactive Map -->
|
|
||||||
<div class="col-lg-5 col-xl-5">
|
|
||||||
<div class="card shadow-sm border-0 map-card-wrapper">
|
|
||||||
<div class="card-header bg-white py-2 px-3 d-flex align-items-center justify-content-between flex-wrap gap-2 border-bottom">
|
|
||||||
<span class="fw-bold text-dark d-flex align-items-center">
|
|
||||||
<i class="bi bi-geo-alt-fill text-success me-2"></i>{% translate "Carte & Aménagements" %}
|
|
||||||
</span>
|
|
||||||
<!-- Map Layer Controls / Legend -->
|
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
||||||
<div class="form-check form-check-inline m-0 map-layer-pill">
|
|
||||||
<input class="form-check-input" type="checkbox" id="toggle-surfaces" checked onchange="toggleNatureLayer('surfaces', this.checked)">
|
|
||||||
<label class="form-check-label" for="toggle-surfaces"><span class="badge bg-success" style="width: 8px; height: 8px; padding: 0; display: inline-block;"></span> {% translate "Surfaces" %}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline m-0 map-layer-pill">
|
|
||||||
<input class="form-check-input" type="checkbox" id="toggle-trees" checked onchange="toggleNatureLayer('trees', this.checked)">
|
|
||||||
<label class="form-check-label" for="toggle-trees"><span class="badge bg-success" style="border-radius: 50%; width: 8px; height: 8px; padding: 0; display: inline-block;"></span> {% translate "Arbres" %}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline m-0 map-layer-pill">
|
|
||||||
<input class="form-check-input" type="checkbox" id="toggle-equipments" checked onchange="toggleNatureLayer('shrubs', this.checked); toggleNatureLayer('benches', this.checked)">
|
|
||||||
<label class="form-check-label" for="toggle-equipments"><span class="badge bg-warning" style="border-radius: 50%; width: 8px; height: 8px; padding: 0; display: inline-block;"></span> {% translate "Autres" %}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0 position-relative">
|
|
||||||
<div id="map"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endblock content%}
|
{% endblock content%}
|
||||||
|
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
{{ feature_lon|json_script:"lon_value" }}
|
{{ section.lon|json_script:"lon_value" }}
|
||||||
{{ feature_lat|json_script:"lat_value" }}
|
{{ section.lat|json_script:"lat_value" }}
|
||||||
{{ section_geojson|json_script:"section_geojson_value" }}
|
{{ section.geojson|json_script:"geojson_value" }}
|
||||||
{{ assets_geojson|json_script:"assets_geojson_value" }}
|
|
||||||
<script>
|
<script>
|
||||||
|
const feature_geojson = JSON.parse(document.getElementById("geojson_value").textContent);
|
||||||
const feature_lon = JSON.parse(document.getElementById("lon_value").textContent);
|
const feature_lon = JSON.parse(document.getElementById("lon_value").textContent);
|
||||||
const feature_lat = JSON.parse(document.getElementById("lat_value").textContent);
|
const feature_lat = JSON.parse(document.getElementById("lat_value").textContent);
|
||||||
const section_geojson = JSON.parse(document.getElementById("section_geojson_value").textContent);
|
|
||||||
const assets_geojson = JSON.parse(document.getElementById("assets_geojson_value").textContent);
|
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static 'common/turf.min.js' %}"></script>
|
<script src="{% static 'assets/green_detail.js' %}"></script>
|
||||||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
|
||||||
<script src="{% static 'assets/nature_location_detail.js' %}"></script>
|
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
122
loko/assets/templates/assets/naturegreensurface_detail.html
Normal file
122
loko/assets/templates/assets/naturegreensurface_detail.html
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-2" style="height: calc(100vh - 200px);">
|
||||||
|
<div class="col-md-8" style="max-height: calc(100vh - 200px); overflow: auto;">
|
||||||
|
<h3 class="h3">
|
||||||
|
<i class="bi bi-suit-club"></i><span class="mx-2">{{ asset.code}}</span>
|
||||||
|
</h3>
|
||||||
|
<hr>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="mx-2">Statut:</label><i class="bi bi-circle-fill status_{{ asset.status }}"></i> {{ asset.get_status_display }}<br>
|
||||||
|
<label class="mx-2">Type:</label>{{ asset.model.name_fr|default:"" }}<br>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if asset.lon %}
|
||||||
|
<a href="https://www.google.com/maps?cbll={{ asset.lat|floatformat:'6u'}},{{ asset.lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid=" target="_blank" style="text-decoration: none; font-size: 16px; display: flex; align-items: center;">
|
||||||
|
<img src="{% static 'common/google-street-view-logo.png' %}" alt="Street View" height="20" style="margin-right: 8px;">
|
||||||
|
View in Google Street View
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="mx-2">Densité:</label>{{ asset.density }}<br>
|
||||||
|
<label class="mx-2">Pondération:</label>{{ asset.weighting }}<br>
|
||||||
|
<label class="mx-2">Fréquence de tonte:</label>{{ asset.weighting }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="mx-2">Dernier entretien:</label>{{ asset.last_preventive_date|default:"" }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Documents -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4>Documents</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
{% for document in asset.documents.all %}
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<a href="{{ document.file.url }}" target="_blank">
|
||||||
|
<div class="img-thumbnail" style="width: 100%; height: 100px; overflow: hidden;">
|
||||||
|
{% if document.thumbnail %}
|
||||||
|
<img src="{{ document.thumbnail.url }}" alt="{{ document.file.name }}" style="width: 100%; height: 100%; object-fit: cover; object-position: center;">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ document.file.url }}" alt="{{ document.file.name }}" style="width: 100%; height: 100%; object-fit: cover; object-position: center;">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Interventions -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4>Interventions</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table id="interventions-table" class="table"
|
||||||
|
data-height="550"
|
||||||
|
data-pagination="true"
|
||||||
|
data-page-size="25"
|
||||||
|
data-click-to-select="true" >
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-field="actions" data-width="100"></th>
|
||||||
|
<th data-field="code">Code</th>
|
||||||
|
<th data-field="title">Titre</th>
|
||||||
|
<th data-field="begin_date">Début</th>
|
||||||
|
<th data-field="statut">Statut</th>
|
||||||
|
</tr>
|
||||||
|
<tbody>
|
||||||
|
{% for interv in interventions %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}{{ interv.id }}" type="button" class="btn btn-sm btn-light detail-btn" data-id="${row.id}" data-bs-toggle="tooltip" title="Detail">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ interv.code }}</td>
|
||||||
|
<td>{{ interv.title }}</td>
|
||||||
|
<td>{{ interv.begin_time|date:"d-m-Y H:i" }}</td>
|
||||||
|
<td>{{ interv.get_status_display }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div id="map">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content%}
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{{ asset.lon|json_script:"lon_value" }}
|
||||||
|
{{ asset.lat|json_script:"lat_value" }}
|
||||||
|
{{ asset.geojson|json_script:"geojson_value" }}
|
||||||
|
<script>
|
||||||
|
const feature_geojson = JSON.parse(document.getElementById("geojson_value").textContent);
|
||||||
|
const feature_lon = JSON.parse(document.getElementById("lon_value").textContent);
|
||||||
|
const feature_lat = JSON.parse(document.getElementById("lat_value").textContent);
|
||||||
|
</script>
|
||||||
|
<script src="{% static 'assets/green_detail.js' %}"></script>
|
||||||
|
{% endblock script %}
|
||||||
|
|
@ -88,24 +88,6 @@
|
||||||
{% if asset.is_remarkable %}
|
{% if asset.is_remarkable %}
|
||||||
<span class="badge bg-warning text-dark ms-1 shadow-sm"><i class="bi bi-star-fill text-warning me-1"></i>{% translate "Arbre remarquable" %}</span>
|
<span class="badge bg-warning text-dark ms-1 shadow-sm"><i class="bi bi-star-fill text-warning me-1"></i>{% translate "Arbre remarquable" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Badge Gestionnaire #}
|
|
||||||
{% if asset.managing_authority %}
|
|
||||||
<span class="badge {% if asset.management_type == 'regional' %}bg-primary-subtle text-primary border border-primary{% else %}bg-success-subtle text-success border border-success{% endif %} ms-1">
|
|
||||||
<i class="bi bi-buildings me-1"></i>{{ asset.managing_authority }}
|
|
||||||
</span>
|
|
||||||
{% elif asset.management_type %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary border ms-1">{{ asset.get_management_type_display }}</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Badges Biologiques Spéciaux #}
|
|
||||||
{% if asset.is_dead %}
|
|
||||||
<span class="badge bg-dark text-white ms-1"><i class="bi bi-x-circle me-1"></i>{% translate "Mort" %}</span>
|
|
||||||
{% elif asset.is_stump %}
|
|
||||||
<span class="badge bg-secondary text-white ms-1"><i class="bi bi-dash-circle me-1"></i>{% translate "Souche" %}</span>
|
|
||||||
{% elif asset.is_chronoxyle %}
|
|
||||||
<span class="badge bg-warning-subtle text-dark border border-warning ms-1"><i class="bi bi-recycle me-1"></i>{% translate "Chronoxyle" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-2 mt-1" style="font-size: 0.8rem; color: #6c757d;">
|
<div class="d-flex align-items-center gap-2 mt-1" style="font-size: 0.8rem; color: #6c757d;">
|
||||||
|
|
@ -177,24 +159,6 @@
|
||||||
{% if asset.is_remarkable %}
|
{% if asset.is_remarkable %}
|
||||||
<span class="badge bg-warning text-dark ms-1 shadow-sm"><i class="bi bi-star-fill text-warning me-1"></i>{% translate "Arbre remarquable" %}</span>
|
<span class="badge bg-warning text-dark ms-1 shadow-sm"><i class="bi bi-star-fill text-warning me-1"></i>{% translate "Arbre remarquable" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Badge Gestionnaire #}
|
|
||||||
{% if asset.managing_authority %}
|
|
||||||
<span class="badge {% if asset.management_type == 'regional' %}bg-primary-subtle text-primary border border-primary{% else %}bg-success-subtle text-success border border-success{% endif %} ms-1">
|
|
||||||
<i class="bi bi-buildings me-1"></i>{{ asset.managing_authority }}
|
|
||||||
</span>
|
|
||||||
{% elif asset.management_type %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary border ms-1">{{ asset.get_management_type_display }}</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Badges Biologiques Spéciaux #}
|
|
||||||
{% if asset.is_dead %}
|
|
||||||
<span class="badge bg-dark text-white ms-1"><i class="bi bi-x-circle me-1"></i>{% translate "Mort" %}</span>
|
|
||||||
{% elif asset.is_stump %}
|
|
||||||
<span class="badge bg-secondary text-white ms-1"><i class="bi bi-dash-circle me-1"></i>{% translate "Souche" %}</span>
|
|
||||||
{% elif asset.is_chronoxyle %}
|
|
||||||
<span class="badge bg-warning-subtle text-dark border border-warning ms-1"><i class="bi bi-recycle me-1"></i>{% translate "Chronoxyle" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-2 mt-1" style="font-size: 0.85rem; color: #6c757d;">
|
<div class="d-flex align-items-center gap-2 mt-1" style="font-size: 0.85rem; color: #6c757d;">
|
||||||
|
|
@ -257,89 +221,6 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Autorité gestionnaire -->
|
|
||||||
<tr class="asset-editable-field" data-field="managing_authority" data-can-edit="{% if 'managing_authority' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Autorité gestionnaire" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display">
|
|
||||||
<span class="field-value fw-semibold text-primary">
|
|
||||||
{% if asset.managing_authority %}
|
|
||||||
<i class="bi bi-bank me-1"></i>{{ asset.managing_authority }}
|
|
||||||
{% if asset.management_type %}
|
|
||||||
<small class="text-muted fw-normal">({{ asset.get_management_type_display }})</small>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{{ asset.get_management_type_display|default:"-" }}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="text" class="form-control form-control-sm" value="{{ asset.managing_authority|default:'' }}" placeholder="{% translate 'Ex: Commune de Watermael-Boitsfort' %}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Type de gestionnaire -->
|
|
||||||
<tr class="asset-editable-field" data-field="management_type" data-can-edit="{% if 'management_type' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Type de gestionnaire" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display"><span class="field-value">{{ asset.get_management_type_display|default:"-" }}</span></div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<select class="form-select form-select-sm">
|
|
||||||
{% for val, lbl in asset.TREE_MANAGEMENT_TYPE_CHOICES %}
|
|
||||||
<option value="{{ val }}" {% if asset.management_type == val %}selected{% endif %}>{{ lbl }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Nom scientifique / Latin -->
|
|
||||||
<tr class="asset-editable-field" data-field="scientific_name" data-can-edit="{% if 'scientific_name' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Nom scientifique / Latin" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display">
|
|
||||||
<span class="field-value fst-italic fw-medium text-success">
|
|
||||||
{{ asset.scientific_name|default:"-" }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="text" class="form-control form-control-sm" value="{{ asset.scientific_name|default:'' }}" placeholder="{% translate 'Ex: Prunus serrulata' %}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Nom vernaculaire / Essence -->
|
|
||||||
<tr class="asset-editable-field" data-field="vernacular_name" data-can-edit="{% if 'vernacular_name' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Nom vernaculaire / Essence" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display"><span class="field-value">{{ asset.vernacular_name|default:"-" }}</span></div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="text" class="form-control form-control-sm" value="{{ asset.vernacular_name|default:'' }}" placeholder="{% translate 'Ex: Cerisier du Japon' %}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Genre & Cultivar -->
|
|
||||||
{% if asset.genus or asset.cultivar or 'genus' in allowed_update_fields %}
|
|
||||||
<tr class="asset-editable-field" data-field="genus" data-can-edit="{% if 'genus' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Genre & Cultivar" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display">
|
|
||||||
<span class="field-value">
|
|
||||||
{{ asset.genus|default:"-" }}{% if asset.cultivar %} '{{ asset.cultivar }}'{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<div class="row g-1">
|
|
||||||
<div class="col-6"><input type="text" class="form-control form-control-sm" placeholder="Genre" value="{{ asset.genus|default:'' }}"></div>
|
|
||||||
<div class="col-6"><input type="text" class="form-control form-control-sm" placeholder="Cultivar" value="{{ asset.cultivar|default:'' }}"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Nom commun FR -->
|
<!-- Nom commun FR -->
|
||||||
<tr class="asset-editable-field" data-field="name_fr" data-can-edit="{% if 'name_fr' in allowed_update_fields %}true{% else %}false{% endif %}">
|
<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 commun (FR)" %}</th>
|
<th>{% translate "Nom commun (FR)" %}</th>
|
||||||
|
|
@ -490,7 +371,7 @@
|
||||||
<span class="field-value text-muted">{{ asset.note|default:"-"|linebreaksbr }}</span>
|
<span class="field-value text-muted">{{ asset.note|default:"-"|linebreaksbr }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-edit d-none">
|
<div class="field-edit d-none">
|
||||||
<textarea class="form-control form-control-sm" rows="3">{{ asset.note|default:'' }}</textarea>
|
<textarea class="form-control form-control-sm" rows="2">{{ asset.note|default:'' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -538,25 +419,6 @@
|
||||||
|
|
||||||
<table class="table table-striped mb-0 align-middle">
|
<table class="table table-striped mb-0 align-middle">
|
||||||
<tbody>
|
<tbody>
|
||||||
<!-- Nombre de troncs / Cépée -->
|
|
||||||
<tr class="asset-editable-field" data-field="number_of_trunks" data-can-edit="{% if 'number_of_trunks' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th style="width: 35%;">{% translate "Nombre de troncs (cépée)" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display">
|
|
||||||
<span class="field-value">
|
|
||||||
{% if asset.number_of_trunks and asset.number_of_trunks > 1 %}
|
|
||||||
<span class="badge bg-secondary-subtle text-secondary border"><i class="bi bi-diagram-3 me-1"></i>{{ asset.number_of_trunks }} {% translate "troncs" %}</span>
|
|
||||||
{% else %}
|
|
||||||
1 {% translate "(tronc unique)" %}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="number" step="1" min="1" class="form-control form-control-sm" value="{{ asset.number_of_trunks|default:1 }}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Hauteur totale -->
|
<!-- Hauteur totale -->
|
||||||
<tr class="asset-editable-field" data-field="height" data-can-edit="{% if 'height' in allowed_update_fields %}true{% else %}false{% endif %}">
|
<tr class="asset-editable-field" data-field="height" data-can-edit="{% if 'height' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||||||
<th style="width: 35%;">{% translate "Hauteur totale (m)" %}</th>
|
<th style="width: 35%;">{% translate "Hauteur totale (m)" %}</th>
|
||||||
|
|
@ -656,173 +518,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{# ── CARTE ÉCO-SERVICES & VALEUR PATRIMONIALE (GRALITY / BARÈME DE L'ARBRE) ── #}
|
|
||||||
<div class="card mb-3 shadow-sm border-success-subtle">
|
|
||||||
<div class="card-header bg-success-subtle d-flex align-items-center justify-content-between">
|
|
||||||
<h5 class="mb-0 fs-6 fw-bold text-success-emphasis">
|
|
||||||
<i class="bi bi-feather me-2"></i>{% translate "Éco-services & Valeur patrimoniale" %}
|
|
||||||
</h5>
|
|
||||||
<span class="badge bg-success text-white">
|
|
||||||
<i class="bi bi-shield-check me-1"></i>{% translate "Indicateurs écologiques" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{# 4 mini-widgets écologiques #}
|
|
||||||
<div class="row g-2 mb-3">
|
|
||||||
<!-- Valeur d'aménité -->
|
|
||||||
<div class="col-6 col-md-3">
|
|
||||||
<div class="p-2 border border-success-subtle rounded text-center bg-white shadow-sm tree-metric-widget">
|
|
||||||
<div class="small text-muted mb-1 metric-title text-truncate" title="{% translate "Valeur d'aménité calculée" %}">
|
|
||||||
<i class="bi bi-currency-euro text-success me-1"></i>{% translate "Aménité" %}
|
|
||||||
</div>
|
|
||||||
<div class="fs-5 fw-bold text-success metric-value">
|
|
||||||
{% if asset.amenity_value %}
|
|
||||||
{{ asset.amenity_value|floatformat:0 }} <small class="text-muted fw-normal" style="font-size: 0.75rem;">€</small>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted fs-6">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stock carbone -->
|
|
||||||
<div class="col-6 col-md-3">
|
|
||||||
<div class="p-2 border border-primary-subtle rounded text-center bg-white shadow-sm tree-metric-widget">
|
|
||||||
<div class="small text-muted mb-1 metric-title text-truncate" title="{% translate 'Stock de carbone séquestré' %}">
|
|
||||||
<i class="bi bi-cloud-check text-primary me-1"></i>{% translate "Carbone" %}
|
|
||||||
</div>
|
|
||||||
<div class="fs-5 fw-bold text-primary metric-value">
|
|
||||||
{% if asset.carbon_stock %}
|
|
||||||
{{ asset.carbon_stock|floatformat:2 }} <small class="text-muted fw-normal" style="font-size: 0.75rem;">t CO₂</small>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted fs-6">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rafraîchissement urbain -->
|
|
||||||
<div class="col-6 col-md-3">
|
|
||||||
<div class="p-2 border border-info-subtle rounded text-center bg-white shadow-sm tree-metric-widget">
|
|
||||||
<div class="small text-muted mb-1 metric-title text-truncate" title="{% translate 'Potentiel de rafraîchissement urbain' %}">
|
|
||||||
<i class="bi bi-thermometer-snow text-info me-1"></i>{% translate "Fraîcheur" %}
|
|
||||||
</div>
|
|
||||||
<div class="fs-5 fw-bold text-info-emphasis metric-value">
|
|
||||||
{% if asset.cooling_indicator %}
|
|
||||||
{{ asset.cooling_indicator|floatformat:1 }} <small class="text-muted fw-normal" style="font-size: 0.75rem;">/ 10</small>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted fs-6">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Indice de biodiversité -->
|
|
||||||
<div class="col-6 col-md-3">
|
|
||||||
<div class="p-2 border border-warning-subtle rounded text-center bg-white shadow-sm tree-metric-widget">
|
|
||||||
<div class="small text-muted mb-1 metric-title text-truncate" title="{% translate "Indice d'accueil biodiversité" %}">
|
|
||||||
<i class="bi bi-bug text-warning me-1"></i>{% translate "Biodiversité" %}
|
|
||||||
</div>
|
|
||||||
<div class="fs-5 fw-bold text-warning-emphasis metric-value">
|
|
||||||
{% if asset.biodiversity_index %}
|
|
||||||
{{ asset.biodiversity_index|floatformat:2 }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted fs-6">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Table détaillée des indicateurs écosystémiques #}
|
|
||||||
<table class="table table-striped mb-0 align-middle">
|
|
||||||
<tbody>
|
|
||||||
<!-- Énergie de refroidissement -->
|
|
||||||
<tr class="asset-editable-field" data-field="cooling_energy_indicator" data-can-edit="{% if 'cooling_energy_indicator' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th style="width: 35%;">{% translate "Énergie de refroidissement (kWh)" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display">
|
|
||||||
<span class="field-value">
|
|
||||||
{% if asset.cooling_energy_indicator %}
|
|
||||||
{{ asset.cooling_energy_indicator|floatformat:1 }} kWh
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="number" step="0.1" class="form-control form-control-sm" value="{{ asset.cooling_energy_indicator|default:'' }}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Coefficient de situation -->
|
|
||||||
<tr class="asset-editable-field" data-field="situation_coefficient" data-can-edit="{% if 'situation_coefficient' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Coefficient de situation (barème)" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display"><span class="field-value">{{ asset.situation_coefficient|default:"-" }}</span></div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="number" step="0.1" class="form-control form-control-sm" value="{{ asset.situation_coefficient|default:'' }}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Coefficient de plantation -->
|
|
||||||
<tr class="asset-editable-field" data-field="plantation_coefficient" data-can-edit="{% if 'plantation_coefficient' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Coefficient de plantation (barème)" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display"><span class="field-value">{{ asset.plantation_coefficient|default:"-" }}</span></div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="number" step="0.1" class="form-control form-control-sm" value="{{ asset.plantation_coefficient|default:'' }}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Date de plantation réelle -->
|
|
||||||
<tr class="asset-editable-field" data-field="planting_date" data-can-edit="{% if 'planting_date' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Date de plantation constatée" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display">
|
|
||||||
<span class="field-value">
|
|
||||||
{% if asset.planting_date %}
|
|
||||||
<i class="bi bi-calendar3 me-1 text-muted"></i>{{ asset.planting_date|date:"d/m/Y" }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="date" class="form-control form-control-sm" value="{{ asset.planting_date|date:'Y-m-d'|default:'' }}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Date de dépérissement / mort -->
|
|
||||||
{% if asset.death_date or asset.is_dead or 'death_date' in allowed_update_fields %}
|
|
||||||
<tr class="asset-editable-field" data-field="death_date" data-can-edit="{% if 'death_date' in allowed_update_fields %}true{% else %}false{% endif %}">
|
|
||||||
<th>{% translate "Date de mort / dépérissement" %}</th>
|
|
||||||
<td>
|
|
||||||
<div class="field-display">
|
|
||||||
<span class="field-value text-danger">
|
|
||||||
{% if asset.death_date %}
|
|
||||||
<i class="bi bi-calendar-x me-1"></i>{{ asset.death_date|date:"d/m/Y" }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="field-edit d-none">
|
|
||||||
<input type="date" class="form-control form-control-sm" value="{{ asset.death_date|date:'Y-m-d'|default:'' }}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{# ── CARTE 3 : DIAGNOSTIC SANITAIRE & SÉCURITÉ ── #}
|
{# ── CARTE 3 : DIAGNOSTIC SANITAIRE & SÉCURITÉ ── #}
|
||||||
<div class="card mb-3 shadow-sm">
|
<div class="card mb-3 shadow-sm">
|
||||||
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
||||||
|
|
|
||||||
|
|
@ -342,25 +342,20 @@
|
||||||
<select name="action_pole_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
<select name="action_pole_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||||
<option value="keep">{% translate "Conserver en DB" %}</option>
|
<option value="keep">{% translate "Conserver en DB" %}</option>
|
||||||
<option value="archive" selected>{% translate "Archiver" %}</option>
|
<option value="archive" selected>{% translate "Archiver" %}</option>
|
||||||
<option value="delete">{% translate "Supprimer de la DB" %}</option>
|
|
||||||
</select>
|
</select>
|
||||||
{% else %}
|
{% else %}
|
||||||
<select name="action_pole_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
<select name="action_pole_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||||
{% if item.status == 'new' %}
|
{% if item.status == 'new' %}
|
||||||
<option value="add" selected>{% translate "Créer l'asset" %}</option>
|
<option value="add" selected>{% translate "Créer l'asset" %}</option>
|
||||||
<option value="keep">{% translate "Ne pas créer (Ignorer)" %}</option>
|
<option value="keep">{% translate "Ignorer" %}</option>
|
||||||
{% elif item.status == 'modified' %}
|
{% elif item.status == 'modified' %}
|
||||||
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
|
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
|
||||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||||
<option value="keep">{% translate "Conserver existant" %}</option>
|
<option value="keep">{% translate "Conserver existant" %}</option>
|
||||||
<option value="archive">{% translate "Archiver l'existant (Ne pas recréer)" %}</option>
|
|
||||||
<option value="delete">{% translate "Supprimer l'existant de la DB" %}</option>
|
|
||||||
{% elif item.status == 'identical' %}
|
{% elif item.status == 'identical' %}
|
||||||
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
||||||
<option value="modify">{% translate "Mettre à jour l'asset" %}</option>
|
<option value="modify">{% translate "Mettre à jour l'asset" %}</option>
|
||||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||||
<option value="archive">{% translate "Archiver l'existant" %}</option>
|
|
||||||
<option value="delete">{% translate "Supprimer l'existant de la DB" %}</option>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -443,25 +438,19 @@
|
||||||
<select name="action_lantern_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
<select name="action_lantern_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||||
<option value="keep">{% translate "Conserver en DB" %}</option>
|
<option value="keep">{% translate "Conserver en DB" %}</option>
|
||||||
<option value="archive" selected>{% translate "Archiver" %}</option>
|
<option value="archive" selected>{% translate "Archiver" %}</option>
|
||||||
<option value="delete">{% translate "Supprimer de la DB" %}</option>
|
|
||||||
</select>
|
</select>
|
||||||
{% else %}
|
{% else %}
|
||||||
<select name="action_lantern_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
<select name="action_lantern_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||||
{% if item.status == 'new' %}
|
{% if item.status == 'new' %}
|
||||||
<option value="add" selected>{% translate "Créer l'asset" %}</option>
|
<option value="add" selected>{% translate "Créer l'asset" %}</option>
|
||||||
<option value="keep">{% translate "Ne pas créer (Ignorer)" %}</option>
|
<option value="keep">{% translate "Ignorer" %}</option>
|
||||||
{% elif item.status == 'modified' %}
|
{% elif item.status == 'modified' %}
|
||||||
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
|
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
|
||||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||||
<option value="keep">{% translate "Conserver existant" %}</option>
|
<option value="keep">{% translate "Conserver existant" %}</option>
|
||||||
<option value="archive">{% translate "Archiver l'existant (Ne pas recréer)" %}</option>
|
|
||||||
<option value="delete">{% translate "Supprimer l'existant de la DB" %}</option>
|
|
||||||
{% elif item.status == 'identical' %}
|
{% elif item.status == 'identical' %}
|
||||||
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
||||||
<option value="modify">{% translate "Mettre à jour l'asset" %}</option>
|
|
||||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||||
<option value="archive">{% translate "Archiver l'existant" %}</option>
|
|
||||||
<option value="delete">{% translate "Supprimer l'existant de la DB" %}</option>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -514,7 +503,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in detectors %}
|
{% for item in detectors %}
|
||||||
<tr class="{% if item.status == 'new' %}table-success-light{% elif item.status == 'modified' %}table-warning-light{% elif item.status == 'db_only' %}{% if item.action_default == 'keep' %}table-light{% else %}table-danger-light{% endif %}{% endif %}">
|
<tr class="{% if item.status == 'new' %}table-success-light{% elif item.status == 'modified' %}table-warning-light{% elif item.status == 'db_only' %}table-danger-light{% endif %}">
|
||||||
<td class="ps-4"><input type="checkbox" name="{% if item.status == 'db_only' %}selected_detector_db_{{ item.code }}{% else %}selected_detector_{{ item.code }}{% endif %}" class="form-check-input detector-checkbox" checked></td>
|
<td class="ps-4"><input type="checkbox" name="{% if item.status == 'db_only' %}selected_detector_db_{{ item.code }}{% else %}selected_detector_{{ item.code }}{% endif %}" class="form-check-input detector-checkbox" checked></td>
|
||||||
<td class="font-weight-medium"><code>{{ item.code }}</code><input type="hidden" name="db_id_detector_{{ item.code }}" value="{{ item.db_id|default:'' }}"></td>
|
<td class="font-weight-medium"><code>{{ item.code }}</code><input type="hidden" name="db_id_detector_{{ item.code }}" value="{{ item.db_id|default:'' }}"></td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -529,12 +518,8 @@
|
||||||
{% elif item.status == 'identical' %}
|
{% elif item.status == 'identical' %}
|
||||||
<span class="badge bg-light text-secondary border">{% translate "Identique" %}</span>
|
<span class="badge bg-light text-secondary border">{% translate "Identique" %}</span>
|
||||||
{% elif item.status == 'db_only' %}
|
{% elif item.status == 'db_only' %}
|
||||||
{% if item.action_default == 'keep' %}
|
|
||||||
<span class="badge bg-secondary">{% translate "DB uniquement" %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">{% translate "DB uniquement" %}</span>
|
<span class="badge bg-danger">{% translate "DB uniquement" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% for diff in item.details_diff %}
|
{% for diff in item.details_diff %}
|
||||||
<div class="small text-muted mt-1" style="font-size: 0.75rem; line-height: 1.25;">
|
<div class="small text-muted mt-1" style="font-size: 0.75rem; line-height: 1.25;">
|
||||||
<i class="bi bi-info-circle me-1"></i>{{ diff }}
|
<i class="bi bi-info-circle me-1"></i>{{ diff }}
|
||||||
|
|
@ -546,27 +531,21 @@
|
||||||
<td>
|
<td>
|
||||||
{% if item.status == 'db_only' %}
|
{% if item.status == 'db_only' %}
|
||||||
<select name="action_detector_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
<select name="action_detector_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||||
<option value="keep" {% if item.action_default == 'keep' %}selected{% endif %}>{% translate "Conserver en DB" %}</option>
|
<option value="keep">{% translate "Conserver en DB" %}</option>
|
||||||
<option value="archive" {% if item.action_default != 'keep' %}selected{% endif %}>{% translate "Archiver" %}</option>
|
<option value="archive" selected>{% translate "Archiver" %}</option>
|
||||||
<option value="delete">{% translate "Supprimer de la DB" %}</option>
|
|
||||||
</select>
|
</select>
|
||||||
{% else %}
|
{% else %}
|
||||||
<select name="action_detector_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
<select name="action_detector_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||||
{% if item.status == 'new' %}
|
{% if item.status == 'new' %}
|
||||||
<option value="add" selected>{% translate "Créer l'asset" %}</option>
|
<option value="add" selected>{% translate "Créer l'asset" %}</option>
|
||||||
<option value="keep">{% translate "Ne pas créer (Ignorer)" %}</option>
|
<option value="keep">{% translate "Ignorer" %}</option>
|
||||||
{% elif item.status == 'modified' %}
|
{% elif item.status == 'modified' %}
|
||||||
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
|
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
|
||||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||||
<option value="keep">{% translate "Conserver existant" %}</option>
|
<option value="keep">{% translate "Conserver existant" %}</option>
|
||||||
<option value="archive">{% translate "Archiver l'existant (Ne pas recréer)" %}</option>
|
|
||||||
<option value="delete">{% translate "Supprimer l'existant de la DB" %}</option>
|
|
||||||
{% elif item.status == 'identical' %}
|
{% elif item.status == 'identical' %}
|
||||||
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
||||||
<option value="modify">{% translate "Mettre à jour l'asset" %}</option>
|
|
||||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||||
<option value="archive">{% translate "Archiver l'existant" %}</option>
|
|
||||||
<option value="delete">{% translate "Supprimer l'existant de la DB" %}</option>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -661,11 +640,6 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-light text-muted border">{% translate "Non tracé" %}</span>
|
<span class="badge bg-light text-muted border">{% translate "Non tracé" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.status != 'db_only' %}
|
|
||||||
<button type="button" class="btn btn-xs btn-outline-primary ms-1 show-cable-on-map-btn" data-code="{{ item.code }}" title="{% translate 'Voir et éditer sur la carte' %}">
|
|
||||||
<i class="bi bi-geo-alt"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></td>
|
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></td>
|
||||||
<td><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
<td><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
||||||
|
|
@ -674,25 +648,20 @@
|
||||||
<select name="action_cable_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
<select name="action_cable_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||||
<option value="keep">{% translate "Conserver en DB" %}</option>
|
<option value="keep">{% translate "Conserver en DB" %}</option>
|
||||||
<option value="archive" selected>{% translate "Archiver" %}</option>
|
<option value="archive" selected>{% translate "Archiver" %}</option>
|
||||||
<option value="delete">{% translate "Supprimer de la DB" %}</option>
|
|
||||||
</select>
|
</select>
|
||||||
{% else %}
|
{% else %}
|
||||||
<select name="action_cable_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
<select name="action_cable_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||||
{% if item.status == 'new' %}
|
{% if item.status == 'new' %}
|
||||||
<option value="add" selected>{% translate "Créer l'asset" %}</option>
|
<option value="add" selected>{% translate "Créer l'asset" %}</option>
|
||||||
<option value="keep">{% translate "Ne pas créer (Ignorer)" %}</option>
|
<option value="keep">{% translate "Ignorer" %}</option>
|
||||||
{% elif item.status == 'modified' %}
|
{% elif item.status == 'modified' %}
|
||||||
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
|
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
|
||||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||||
<option value="keep">{% translate "Conserver existant" %}</option>
|
<option value="keep">{% translate "Conserver existant" %}</option>
|
||||||
<option value="archive">{% translate "Archiver l'existant (Ne pas recréer)" %}</option>
|
|
||||||
<option value="delete">{% translate "Supprimer l'existant de la DB" %}</option>
|
|
||||||
{% elif item.status == 'identical' %}
|
{% elif item.status == 'identical' %}
|
||||||
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
||||||
<option value="modify">{% translate "Mettre à jour l'asset" %}</option>
|
<option value="modify">{% translate "Mettre à jour l'asset" %}</option>
|
||||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||||
<option value="archive">{% translate "Archiver l'existant" %}</option>
|
|
||||||
<option value="delete">{% translate "Supprimer l'existant de la DB" %}</option>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -780,7 +749,6 @@
|
||||||
<select name="action_other_{{ item.unique_key }}" class="form-select form-select-sm rounded-2">
|
<select name="action_other_{{ item.unique_key }}" class="form-select form-select-sm rounded-2">
|
||||||
<option value="keep" selected>{% translate "Conserver (Par défaut)" %}</option>
|
<option value="keep" selected>{% translate "Conserver (Par défaut)" %}</option>
|
||||||
<option value="archive">{% translate "Archiver" %}</option>
|
<option value="archive">{% translate "Archiver" %}</option>
|
||||||
<option value="delete">{% translate "Supprimer de la DB" %}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -879,19 +847,13 @@
|
||||||
<!-- Map Cables Sub-pane -->
|
<!-- Map Cables Sub-pane -->
|
||||||
<div class="tab-pane fade h-100" id="map-cables-pane" role="tabpanel" aria-labelledby="map-cables-tab">
|
<div class="tab-pane fade h-100" id="map-cables-pane" role="tabpanel" aria-labelledby="map-cables-tab">
|
||||||
<div class="d-flex flex-column h-100 align-items-start justify-content-start">
|
<div class="d-flex flex-column h-100 align-items-start justify-content-start">
|
||||||
<div class="mb-2 p-2.5 bg-light rounded-3 border w-100 shadow-sm" style="font-size: 0.78rem;">
|
<div class="mb-2 p-2 bg-light rounded-3 border w-100">
|
||||||
<div class="d-flex align-items-center justify-content-between mb-1.5 pb-1 border-bottom">
|
<div class="font-weight-semibold text-success mb-1">
|
||||||
<span class="font-weight-semibold text-success d-flex align-items-center gap-1">
|
<i class="bi bi-bezier"></i> {% translate "Tracé des câbles" %}
|
||||||
<i class="bi bi-bezier"></i> {% translate "Aide au tracé des câbles" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled mb-0 text-secondary d-flex flex-column gap-1" style="line-height: 1.35;">
|
<p class="text-secondary mb-0 small">
|
||||||
<li><i class="bi bi-plus-circle text-primary me-1"></i><strong>{% translate "Tracer" %} :</strong> {% translate "Double-clic sur la carte pour ajouter un point." %}</li>
|
{% translate "Cliquez sur 'Tracer / Éditer' puis double-cliquez sur la carte pour tracer la ligne du câble." %}
|
||||||
<li><i class="bi bi-node-plus text-success me-1"></i><strong>{% translate "Ajouter un nœud" %} :</strong> {% translate "Double-clic sur un segment du câble." %}</li>
|
</p>
|
||||||
<li><i class="bi bi-node-minus text-danger me-1"></i><strong>{% translate "Supprimer un nœud" %} :</strong> {% translate "Double-clic sur un nœud (poignée orange)." %}</li>
|
|
||||||
<li><i class="bi bi-arrows-move text-warning me-1"></i><strong>{% translate "Déplacer" %} :</strong> {% translate "Glisser-déposer les poignées pour ajuster." %}</li>
|
|
||||||
<li><i class="bi bi-files text-info me-1"></i><strong>{% translate "Copier tracé" %} :</strong> {% translate "Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)." %}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-group list-group-flush overflow-auto flex-grow-1 w-100 align-self-stretch">
|
<div class="list-group list-group-flush overflow-auto flex-grow-1 w-100 align-self-stretch">
|
||||||
|
|
@ -900,23 +862,12 @@
|
||||||
<div class="list-group-item py-2 px-1 border-bottom d-flex flex-column gap-1 cable-config-item align-self-stretch" data-code="{{ item.code }}">
|
<div class="list-group-item py-2 px-1 border-bottom d-flex flex-column gap-1 cable-config-item align-self-stretch" data-code="{{ item.code }}">
|
||||||
<div class="d-flex align-items-center justify-content-between w-100">
|
<div class="d-flex align-items-center justify-content-between w-100">
|
||||||
<strong class="text-dark font-monospace">{{ item.name }}</strong>
|
<strong class="text-dark font-monospace">{{ item.name }}</strong>
|
||||||
<span class="badge cable-geom-status-badge font-monospace" id="badge-cable-status-{{ item.code }}" style="font-size: 0.72rem;">
|
|
||||||
<span class="text-muted">…</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small font-monospace">{{ item.deduced_model_name }}</div>
|
<div class="text-muted small font-monospace">{{ item.deduced_model_name }}</div>
|
||||||
<div class="mt-1 d-flex align-items-center gap-1 flex-wrap">
|
<div class="mt-1 align-self-start">
|
||||||
<button type="button" class="btn btn-xs btn-outline-success py-0.5 px-2 edit-cable-btn rounded-2" data-code="{{ item.code }}">
|
<button type="button" class="btn btn-xs btn-outline-success py-0.5 px-2 edit-cable-btn rounded-2" data-code="{{ item.code }}">
|
||||||
<i class="bi bi-pencil-square"></i> {% translate "Tracer / Éditer" %}
|
<i class="bi bi-pencil-square"></i> {% translate "Tracer / Éditer" %}
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown d-inline-block">
|
|
||||||
<button type="button" class="btn btn-xs btn-outline-secondary dropdown-toggle py-0.5 px-2 rounded-2 copy-cable-btn" data-bs-toggle="dropdown" aria-expanded="false" title="{% translate "Copier le tracé d'un autre câble" %}">
|
|
||||||
<i class="bi bi-files"></i> {% translate "Copier tracé…" %}
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm copy-cable-menu py-1" data-target-code="{{ item.code }}" style="font-size: 0.8rem; min-width: 220px;">
|
|
||||||
<!-- Populated dynamically by JS -->
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -936,17 +887,10 @@
|
||||||
<!-- Mode indicator centered at the top -->
|
<!-- Mode indicator centered at the top -->
|
||||||
<div class="position-absolute top-0 start-50 translate-middle-x m-3 z-3 bg-white py-1 px-3 rounded-pill shadow border d-flex align-items-center gap-2" style="white-space: nowrap;">
|
<div class="position-absolute top-0 start-50 translate-middle-x m-3 z-3 bg-white py-1 px-3 rounded-pill shadow border d-flex align-items-center gap-2" style="white-space: nowrap;">
|
||||||
<span class="small text-muted font-monospace font-weight-semibold mb-0" id="map-mode-indicator">{% translate "Mode: Lecture" %}</span>
|
<span class="small text-muted font-monospace font-weight-semibold mb-0" id="map-mode-indicator">{% translate "Mode: Lecture" %}</span>
|
||||||
<button type="button" id="btn-duplicate-trace" class="btn btn-xs btn-outline-primary rounded-pill py-0.5 px-2" style="display: none; font-size: 0.75rem;">
|
|
||||||
<i class="bi bi-copy"></i> {% translate "Appliquer aux autres câbles…" %}
|
|
||||||
</button>
|
|
||||||
<button type="button" id="btn-clear-trace" class="btn btn-xs btn-outline-danger rounded-pill py-0.5 px-2" style="display: none; font-size: 0.75rem;">
|
<button type="button" id="btn-clear-trace" class="btn btn-xs btn-outline-danger rounded-pill py-0.5 px-2" style="display: none; font-size: 0.75rem;">
|
||||||
<i class="bi bi-trash"></i> {% translate "Effacer" %}
|
<i class="bi bi-trash"></i> {% translate "Effacer" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cable-edit-tip" class="position-absolute start-50 translate-middle-x z-2 px-3 py-1 bg-white bg-opacity-95 rounded-pill shadow-sm border small text-muted text-center" style="top: 56px; display: none; font-size: 0.75rem; white-space: nowrap;">
|
|
||||||
<i class="bi bi-info-circle text-primary me-1"></i>
|
|
||||||
<span>{% translate "Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud pour le supprimer • Glisser pour déplacer" %}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="map" class="w-100 h-100"></div>
|
<div id="map" class="w-100 h-100"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -955,46 +899,6 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Modal: Appliquer le tracé d'un câble à d'autres câbles -->
|
|
||||||
<div class="modal fade" id="duplicateCableModal" tabindex="-1" aria-labelledby="duplicateCableModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content rounded-4 border-0 shadow">
|
|
||||||
<div class="modal-header border-bottom py-2.5 px-3">
|
|
||||||
<h6 class="modal-title font-weight-semibold text-dark d-flex align-items-center gap-2 mb-0" id="duplicateCableModalLabel">
|
|
||||||
<i class="bi bi-copy text-primary"></i> {% translate "Appliquer le tracé à d'autres câbles" %}
|
|
||||||
</h6>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body p-3">
|
|
||||||
<p class="text-secondary small mb-3">
|
|
||||||
{% translate "Copier le tracé de" %} <strong id="modal-source-cable-name" class="text-dark"></strong> <span id="modal-source-cable-info" class="badge bg-light text-secondary border"></span> {% translate "vers les câbles sélectionnés ci-dessous :" %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="p-2.5 bg-light rounded-3 border mb-2">
|
|
||||||
<div class="form-check mb-0">
|
|
||||||
<input class="form-check-input" type="checkbox" id="modal-select-all-cables">
|
|
||||||
<label class="form-check-label font-weight-semibold small text-dark cursor-pointer" for="modal-select-all-cables">
|
|
||||||
{% translate "Tout sélectionner" %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="modal-target-cables-list" class="d-flex flex-column gap-1.5 overflow-auto pe-1" style="max-height: 240px;">
|
|
||||||
<!-- Dynamically generated list of cables -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer border-top py-2 px-3 d-flex justify-content-between">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary rounded-2" data-bs-dismiss="modal">
|
|
||||||
{% translate "Annuler" %}
|
|
||||||
</button>
|
|
||||||
<button type="button" id="btn-confirm-duplicate-cable" class="btn btn-sm btn-primary rounded-2 px-3 d-flex align-items-center gap-1.5">
|
|
||||||
<i class="bi bi-check-lg"></i> {% translate "Copier le tracé" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1153,7 +1057,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const tableView = document.getElementById('table-view');
|
const tableView = document.getElementById('table-view');
|
||||||
const mapView = document.getElementById('map-view');
|
const mapView = document.getElementById('map-view');
|
||||||
|
|
||||||
function switchToMapView(targetTab, cableCodeToEdit) {
|
document.querySelectorAll('.show-map-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetTab = this.dataset.targetTab;
|
||||||
|
|
||||||
tableView.classList.remove('d-block');
|
tableView.classList.remove('d-block');
|
||||||
tableView.classList.add('d-none');
|
tableView.classList.add('d-none');
|
||||||
|
|
||||||
|
|
@ -1172,28 +1080,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
} else if (targetTab === 'cables') {
|
} else if (targetTab === 'cables') {
|
||||||
const tabEl = document.getElementById('map-cables-tab');
|
const tabEl = document.getElementById('map-cables-tab');
|
||||||
if (tabEl) new bootstrap.Tab(tabEl).show();
|
if (tabEl) new bootstrap.Tab(tabEl).show();
|
||||||
if (cableCodeToEdit) {
|
|
||||||
activateCableEditingMode(cableCodeToEdit);
|
|
||||||
const item = document.querySelector(`.cable-config-item[data-code="${cableCodeToEdit}"]`);
|
|
||||||
if (item) {
|
|
||||||
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('.show-map-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
switchToMapView(this.dataset.targetTab);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.show-cable-on-map-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
switchToMapView('cables', this.dataset.code);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1264,11 +1151,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
name: "{{ item.name|escapejs }}",
|
name: "{{ item.name|escapejs }}",
|
||||||
status: "{{ item.status|escapejs }}",
|
status: "{{ item.status|escapejs }}",
|
||||||
geom_modified: {{ item.geom_modified|yesno:"true,false" }},
|
geom_modified: {{ item.geom_modified|yesno:"true,false" }},
|
||||||
connected_poles: {{ item.connected_poles|safe|default:"[]" }},
|
connected_poles: {{ item.connected_poles|safe|default:"[]" }}
|
||||||
db_coords: {{ item.db_coords|safe|default:"[]" }},
|
|
||||||
has_db_geom: {{ item.has_db_geom|yesno:"true,false" }},
|
|
||||||
db_geom_length: {% if item.db_geom_length is not None %}{{ item.db_geom_length|unlocalize }}{% else %}null{% endif %},
|
|
||||||
deduced_model_name: "{{ item.deduced_model_name|default:''|escapejs }}"
|
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
@ -1303,87 +1186,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const placedPoles = {};
|
const placedPoles = {};
|
||||||
let layerSwitcher = null;
|
let layerSwitcher = null;
|
||||||
|
|
||||||
function calculateLineLengthMeters(coords) {
|
|
||||||
if (!coords || coords.length < 2) return 0;
|
|
||||||
const R = 6371000;
|
|
||||||
let total = 0;
|
|
||||||
for (let i = 0; i < coords.length - 1; i++) {
|
|
||||||
const lon1 = coords[i][0] * Math.PI / 180;
|
|
||||||
const lat1 = coords[i][1] * Math.PI / 180;
|
|
||||||
const lon2 = coords[i+1][0] * Math.PI / 180;
|
|
||||||
const lat2 = coords[i+1][1] * Math.PI / 180;
|
|
||||||
const dLat = lat2 - lat1;
|
|
||||||
const dLon = lon2 - lon1;
|
|
||||||
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
|
||||||
Math.cos(lat1) * Math.cos(lat2) *
|
|
||||||
Math.sin(dLon/2) * Math.sin(dLon/2);
|
|
||||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
||||||
total += R * c;
|
|
||||||
}
|
|
||||||
return Math.round(total * 10) / 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCableCoords(code) {
|
|
||||||
if (cableGeometries[code] && cableGeometries[code].length >= 2) {
|
|
||||||
return cableGeometries[code];
|
|
||||||
}
|
|
||||||
const c = cablesList.find(item => item.code === code);
|
|
||||||
if (!c) return [];
|
|
||||||
if (c.db_coords && c.db_coords.length >= 2) {
|
|
||||||
return c.db_coords;
|
|
||||||
}
|
|
||||||
const coords = [];
|
|
||||||
const connected = c.connected_poles || [];
|
|
||||||
connected.forEach(pCode => {
|
|
||||||
if (placedPoles[pCode]) {
|
|
||||||
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return coords;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCableBadge(code) {
|
|
||||||
const badge = document.getElementById(`badge-cable-status-${code}`);
|
|
||||||
const coords = getCableCoords(code);
|
|
||||||
const c = cablesList.find(item => item.code === code);
|
|
||||||
if (!badge || !c) return;
|
|
||||||
|
|
||||||
if (coords && coords.length >= 2) {
|
|
||||||
const len = calculateLineLengthMeters(coords);
|
|
||||||
const isModified = cableGeometries[code] !== undefined || c.geom_modified;
|
|
||||||
if (isModified) {
|
|
||||||
badge.className = 'badge bg-warning text-dark border border-warning-subtle font-monospace cable-geom-status-badge';
|
|
||||||
badge.innerHTML = `<i class="bi bi-bezier2"></i> ${coords.length} pts (${len} m)`;
|
|
||||||
} else {
|
|
||||||
badge.className = 'badge bg-info-subtle text-info-emphasis border border-info-subtle font-monospace cable-geom-status-badge';
|
|
||||||
badge.innerHTML = `<i class="bi bi-bezier2"></i> ${coords.length} pts (${len} m)`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
badge.className = 'badge bg-light text-muted border font-monospace cable-geom-status-badge';
|
|
||||||
badge.textContent = `{% translate "Non tracé" %}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTableCableGeomCell(code) {
|
|
||||||
const geomCell = document.getElementById(`geom_cell_cable_${code}`);
|
|
||||||
if (!geomCell) return;
|
|
||||||
const coords = getCableCoords(code);
|
|
||||||
if (coords && coords.length >= 2) {
|
|
||||||
const len = calculateLineLengthMeters(coords);
|
|
||||||
geomCell.innerHTML = `<span class="badge bg-warning text-dark d-inline-flex align-items-center gap-1"><i class="bi bi-arrows-move"></i> {% translate "Tracé modifié" %} (${len} m)</span> <button type="button" class="btn btn-xs btn-outline-primary ms-1 show-cable-on-map-btn" data-code="${code}" title="{% translate 'Voir et éditer sur la carte' %}"><i class="bi bi-geo-alt"></i></button>`;
|
|
||||||
} else {
|
|
||||||
geomCell.innerHTML = `<span class="badge bg-light text-muted border">{% translate "Non tracé" %}</span> <button type="button" class="btn btn-xs btn-outline-primary ms-1 show-cable-on-map-btn" data-code="${code}" title="{% translate 'Tracer sur la carte' %}"><i class="bi bi-geo-alt"></i></button>`;
|
|
||||||
}
|
|
||||||
const btn = geomCell.querySelector('.show-cable-on-map-btn');
|
|
||||||
if (btn) {
|
|
||||||
btn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
switchToMapView('cables', code);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function markCableAsModified(code) {
|
function markCableAsModified(code) {
|
||||||
const selectAction = document.querySelector(`select[name="action_cable_${code}"]`);
|
const selectAction = document.querySelector(`select[name="action_cable_${code}"]`);
|
||||||
if (selectAction) {
|
if (selectAction) {
|
||||||
|
|
@ -1396,8 +1198,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const cb = document.querySelector(`input[name="selected_cable_${code}"]`);
|
const cb = document.querySelector(`input[name="selected_cable_${code}"]`);
|
||||||
if (cb) cb.checked = true;
|
if (cb) cb.checked = true;
|
||||||
|
|
||||||
updateTableCableGeomCell(code);
|
const geomCell = document.getElementById(`geom_cell_cable_${code}`);
|
||||||
updateCableBadge(code);
|
if (geomCell) {
|
||||||
|
geomCell.innerHTML = `<span class="badge bg-warning text-dark d-inline-flex align-items-center gap-1"><i class="bi bi-arrows-move"></i> {% translate "Tracé modifié" %}</span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapCandidates = initialDxfPoles.map(dp => [dp.lon, dp.lat, dp.x_3812, dp.y_3812]);
|
const snapCandidates = initialDxfPoles.map(dp => [dp.lon, dp.lat, dp.x_3812, dp.y_3812]);
|
||||||
|
|
@ -1887,12 +1691,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
deactivateCableEditingMode();
|
deactivateCableEditingMode();
|
||||||
|
|
||||||
activeEditCable = code;
|
activeEditCable = code;
|
||||||
const cableInfo = cablesList.find(c => c.code === code);
|
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Tracé" %} ${cablesList.find(c => c.code === code).name}`;
|
||||||
const cableName = cableInfo ? cableInfo.name : code;
|
|
||||||
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Tracé" %} ${cableName}`;
|
|
||||||
document.getElementById('btn-clear-trace').style.display = 'block';
|
document.getElementById('btn-clear-trace').style.display = 'block';
|
||||||
document.getElementById('btn-duplicate-trace').style.display = 'block';
|
|
||||||
document.getElementById('cable-edit-tip').style.display = 'block';
|
|
||||||
|
|
||||||
const mapPane = document.getElementById('map-cables-pane');
|
const mapPane = document.getElementById('map-cables-pane');
|
||||||
mapPane.querySelectorAll('.cable-config-item').forEach(item => item.classList.remove('bg-light'));
|
mapPane.querySelectorAll('.cable-config-item').forEach(item => item.classList.remove('bg-light'));
|
||||||
|
|
@ -1901,21 +1701,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
let coords = cableGeometries[code];
|
let coords = cableGeometries[code];
|
||||||
if (!coords || coords.length === 0) {
|
if (!coords || coords.length === 0) {
|
||||||
const defaultCoords = getCableCoords(code);
|
const cableInfo = cablesList.find(c => c.code === code);
|
||||||
coords = defaultCoords && defaultCoords.length > 0 ? defaultCoords.map(pt => [pt[0], pt[1]]) : [];
|
const connected = cableInfo.connected_poles || [];
|
||||||
|
|
||||||
|
coords = [];
|
||||||
|
connected.forEach(pCode => {
|
||||||
|
if (placedPoles[pCode]) {
|
||||||
|
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
|
||||||
|
}
|
||||||
|
});
|
||||||
cableGeometries[code] = coords;
|
cableGeometries[code] = coords;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawCableHandles(code);
|
drawCableHandles(code);
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
updateCableBadge(code);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deactivateCableEditingMode() {
|
function deactivateCableEditingMode() {
|
||||||
activeEditCable = null;
|
activeEditCable = null;
|
||||||
document.getElementById('btn-clear-trace').style.display = 'none';
|
document.getElementById('btn-clear-trace').style.display = 'none';
|
||||||
document.getElementById('btn-duplicate-trace').style.display = 'none';
|
|
||||||
document.getElementById('cable-edit-tip').style.display = 'none';
|
|
||||||
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Lecture" %}`;
|
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Lecture" %}`;
|
||||||
|
|
||||||
cableHandles.forEach(h => h.remove());
|
cableHandles.forEach(h => h.remove());
|
||||||
|
|
@ -1964,7 +1768,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(code);
|
markCableAsModified(code);
|
||||||
populateCopyCableMenus();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('dblclick', function(e) {
|
el.addEventListener('dblclick', function(e) {
|
||||||
|
|
@ -1974,7 +1777,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(code);
|
markCableAsModified(code);
|
||||||
populateCopyCableMenus();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2032,7 +1834,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(activeEditCable);
|
markCableAsModified(activeEditCable);
|
||||||
populateCopyCableMenus();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on('dblclick', function(e) {
|
map.on('dblclick', function(e) {
|
||||||
|
|
@ -2056,7 +1857,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(activeEditCable);
|
markCableAsModified(activeEditCable);
|
||||||
populateCopyCableMenus();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hover mouse styles for active cable line
|
// Hover mouse styles for active cable line
|
||||||
|
|
@ -2080,192 +1880,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(activeEditCable);
|
markCableAsModified(activeEditCable);
|
||||||
populateCopyCableMenus();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function copyCableGeometry(sourceCode, targetCode) {
|
|
||||||
const srcCoords = getCableCoords(sourceCode);
|
|
||||||
if (!srcCoords || srcCoords.length < 2) {
|
|
||||||
alert("{% translate "Le câble source ne possède aucun tracé valide à copier." %}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
cableGeometries[targetCode] = srcCoords.map(pt => [pt[0], pt[1]]);
|
|
||||||
saveCablesToForm();
|
|
||||||
markCableAsModified(targetCode);
|
|
||||||
populateCopyCableMenus();
|
|
||||||
if (activeEditCable === targetCode) {
|
|
||||||
drawCableHandles(targetCode);
|
|
||||||
redrawCablesOnMap();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateCopyCableMenus() {
|
|
||||||
document.querySelectorAll('.copy-cable-menu').forEach(menu => {
|
|
||||||
const targetCode = menu.dataset.targetCode;
|
|
||||||
menu.innerHTML = '';
|
|
||||||
|
|
||||||
let hasAvailable = false;
|
|
||||||
cablesList.forEach(other => {
|
|
||||||
if (other.code === targetCode || other.status === 'db_only') return;
|
|
||||||
const coords = getCableCoords(other.code);
|
|
||||||
if (coords && coords.length >= 2) {
|
|
||||||
hasAvailable = true;
|
|
||||||
const len = calculateLineLengthMeters(coords);
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.innerHTML = `
|
|
||||||
<button type="button" class="dropdown-item py-1.5 px-3 d-flex align-items-center justify-content-between copy-from-cable-btn" data-source="${other.code}" data-target="${targetCode}">
|
|
||||||
<span class="font-monospace text-truncate me-2">${other.name}</span>
|
|
||||||
<span class="badge bg-light text-secondary border font-monospace" style="font-size: 0.7rem;">${coords.length} pts (${len} m)</span>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
menu.appendChild(li);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasAvailable) {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.className = 'dropdown-item text-muted small fst-italic py-2';
|
|
||||||
li.textContent = `{% translate "Aucun tracé disponible" %}`;
|
|
||||||
menu.appendChild(li);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rebind click listeners for dynamically populated dropdown items
|
|
||||||
document.querySelectorAll('.copy-from-cable-btn').forEach(btn => {
|
|
||||||
btn.onclick = function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const source = this.dataset.source;
|
|
||||||
const target = this.dataset.target;
|
|
||||||
const success = copyCableGeometry(source, target);
|
|
||||||
if (success) {
|
|
||||||
const targetCable = cablesList.find(c => c.code === target);
|
|
||||||
const sourceCable = cablesList.find(c => c.code === source);
|
|
||||||
const targetName = targetCable ? targetCable.name : target;
|
|
||||||
const sourceName = sourceCable ? sourceCable.name : source;
|
|
||||||
const tipEl = document.getElementById('cable-edit-tip');
|
|
||||||
if (tipEl) {
|
|
||||||
tipEl.style.display = 'block';
|
|
||||||
const origContent = tipEl.innerHTML;
|
|
||||||
tipEl.innerHTML = `<i class="bi bi-check-circle-fill text-success me-1"></i><strong>{% translate "Tracé copié de" %} ${sourceName} {% translate "vers" %} ${targetName}</strong>`;
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!activeEditCable) tipEl.style.display = 'none';
|
|
||||||
else tipEl.innerHTML = origContent;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modal Duplicate Trace logic
|
|
||||||
const duplicateModalEl = document.getElementById('duplicateCableModal');
|
|
||||||
const duplicateModal = duplicateModalEl ? new bootstrap.Modal(duplicateModalEl) : null;
|
|
||||||
|
|
||||||
const btnDuplicateTrace = document.getElementById('btn-duplicate-trace');
|
|
||||||
if (btnDuplicateTrace) {
|
|
||||||
btnDuplicateTrace.addEventListener('click', function() {
|
|
||||||
if (!activeEditCable) return;
|
|
||||||
const currentCable = cablesList.find(c => c.code === activeEditCable);
|
|
||||||
const coords = getCableCoords(activeEditCable);
|
|
||||||
if (!coords || coords.length < 2) {
|
|
||||||
alert("{% translate "Veuillez d'abord tracer au moins deux points pour ce câble avant de le dupliquer." %}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const len = calculateLineLengthMeters(coords);
|
|
||||||
const srcNameEl = document.getElementById('modal-source-cable-name');
|
|
||||||
const srcInfoEl = document.getElementById('modal-source-cable-info');
|
|
||||||
if (srcNameEl) srcNameEl.textContent = currentCable ? currentCable.name : activeEditCable;
|
|
||||||
if (srcInfoEl) srcInfoEl.textContent = `${coords.length} pts • ${len} m`;
|
|
||||||
|
|
||||||
const listContainer = document.getElementById('modal-target-cables-list');
|
|
||||||
if (listContainer) {
|
|
||||||
listContainer.innerHTML = '';
|
|
||||||
const otherCables = cablesList.filter(c => c.code !== activeEditCable && c.status !== 'db_only');
|
|
||||||
if (otherCables.length === 0) {
|
|
||||||
listContainer.innerHTML = `<div class="text-center text-muted small py-3">{% translate "Aucun autre câble disponible dans ce carrefour." %}</div>`;
|
|
||||||
const confirmBtn = document.getElementById('btn-confirm-duplicate-cable');
|
|
||||||
if (confirmBtn) confirmBtn.disabled = true;
|
|
||||||
} else {
|
|
||||||
const confirmBtn = document.getElementById('btn-confirm-duplicate-cable');
|
|
||||||
if (confirmBtn) confirmBtn.disabled = false;
|
|
||||||
otherCables.forEach(c => {
|
|
||||||
const cCoords = getCableCoords(c.code);
|
|
||||||
const hasGeom = cCoords && cCoords.length >= 2;
|
|
||||||
const cLen = hasGeom ? calculateLineLengthMeters(cCoords) : null;
|
|
||||||
const geomBadge = hasGeom
|
|
||||||
? `<span class="badge bg-light text-secondary border font-monospace ms-auto" style="font-size: 0.7rem;">${cCoords.length} pts (${cLen} m)</span>`
|
|
||||||
: `<span class="badge bg-light text-muted border font-monospace ms-auto" style="font-size: 0.7rem;">{% translate "Non tracé" %}</span>`;
|
|
||||||
|
|
||||||
const itemDiv = document.createElement('div');
|
|
||||||
itemDiv.className = 'form-check d-flex align-items-center p-2 rounded border bg-white';
|
|
||||||
itemDiv.innerHTML = `
|
|
||||||
<input class="form-check-input modal-cable-target-cb me-2" type="checkbox" value="${c.code}" id="modal_cb_target_${c.code}">
|
|
||||||
<label class="form-check-label small text-dark d-flex align-items-center w-100 cursor-pointer mb-0" for="modal_cb_target_${c.code}">
|
|
||||||
<span class="font-monospace font-weight-semibold me-2">${c.name}</span>
|
|
||||||
<span class="text-muted small">${c.deduced_model_name ? '(' + c.deduced_model_name + ')' : ''}</span>
|
|
||||||
${geomBadge}
|
|
||||||
</label>
|
|
||||||
`;
|
|
||||||
listContainer.appendChild(itemDiv);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectAllCb = document.getElementById('modal-select-all-cables');
|
|
||||||
if (selectAllCb) selectAllCb.checked = false;
|
|
||||||
|
|
||||||
if (duplicateModal) duplicateModal.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const modalSelectAllCables = document.getElementById('modal-select-all-cables');
|
|
||||||
if (modalSelectAllCables) {
|
|
||||||
modalSelectAllCables.addEventListener('change', function() {
|
|
||||||
const checked = this.checked;
|
|
||||||
document.querySelectorAll('.modal-cable-target-cb').forEach(cb => {
|
|
||||||
cb.checked = checked;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const btnConfirmDuplicateCable = document.getElementById('btn-confirm-duplicate-cable');
|
|
||||||
if (btnConfirmDuplicateCable) {
|
|
||||||
btnConfirmDuplicateCable.addEventListener('click', function() {
|
|
||||||
if (!activeEditCable) return;
|
|
||||||
const selectedCbs = document.querySelectorAll('.modal-cable-target-cb:checked');
|
|
||||||
if (selectedCbs.length === 0) {
|
|
||||||
alert("{% translate "Veuillez sélectionner au moins un câble cible." %}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let appliedCount = 0;
|
|
||||||
selectedCbs.forEach(cb => {
|
|
||||||
const targetCode = cb.value;
|
|
||||||
copyCableGeometry(activeEditCable, targetCode);
|
|
||||||
appliedCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicateModal) duplicateModal.hide();
|
|
||||||
|
|
||||||
const tipEl = document.getElementById('cable-edit-tip');
|
|
||||||
if (tipEl) {
|
|
||||||
const origContent = tipEl.innerHTML;
|
|
||||||
tipEl.innerHTML = `<i class="bi bi-check-circle-fill text-success me-1"></i><strong>${appliedCount} {% translate "câble(s) mis à jour avec ce tracé !" %}</strong>`;
|
|
||||||
setTimeout(() => {
|
|
||||||
if (activeEditCable) tipEl.innerHTML = origContent;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCablesToForm() {
|
function saveCablesToForm() {
|
||||||
cablesList.forEach(c => {
|
cablesList.forEach(c => {
|
||||||
let coords = cableGeometries[c.code];
|
let coords = cableGeometries[c.code];
|
||||||
if (!coords || coords.length === 0) {
|
if (!coords || coords.length === 0) {
|
||||||
coords = getCableCoords(c.code);
|
coords = [];
|
||||||
|
const connected = c.connected_poles || [];
|
||||||
|
connected.forEach(pCode => {
|
||||||
|
if (placedPoles[pCode]) {
|
||||||
|
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const input = document.getElementById(`geom_cable_${c.code}`);
|
const input = document.getElementById(`geom_cable_${c.code}`);
|
||||||
if (input && coords && coords.length >= 2) {
|
if (input && coords && coords.length >= 2) {
|
||||||
|
|
@ -2298,7 +1926,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
let coords = cableGeometries[c.code];
|
let coords = cableGeometries[c.code];
|
||||||
if (!coords || coords.length === 0) {
|
if (!coords || coords.length === 0) {
|
||||||
coords = getCableCoords(c.code);
|
coords = [];
|
||||||
|
const connected = c.connected_poles || [];
|
||||||
|
connected.forEach(pCode => {
|
||||||
|
if (placedPoles[pCode]) {
|
||||||
|
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coords && coords.length >= 2) {
|
if (coords && coords.length >= 2) {
|
||||||
|
|
@ -2326,12 +1960,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
map.triggerRepaint();
|
map.triggerRepaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial cable badges and copy menus update
|
|
||||||
cablesList.forEach(c => {
|
|
||||||
updateCableBadge(c.code);
|
|
||||||
});
|
|
||||||
populateCopyCableMenus();
|
|
||||||
|
|
||||||
// ── Georeferenced Plans & DXF Background Loader ──
|
// ── Georeferenced Plans & DXF Background Loader ──
|
||||||
const georeferencedDocumentsDataEl = document.getElementById('georeferencedDocumentsData');
|
const georeferencedDocumentsDataEl = document.getElementById('georeferencedDocumentsData');
|
||||||
let georeferencedDocuments = [];
|
let georeferencedDocuments = [];
|
||||||
|
|
|
||||||
|
|
@ -480,7 +480,7 @@
|
||||||
text-transform: uppercase !important;
|
text-transform: uppercase !important;
|
||||||
letter-spacing: 0.04em !important;
|
letter-spacing: 0.04em !important;
|
||||||
border-bottom: 1.5px solid #e2e8f0 !important;
|
border-bottom: 1.5px solid #e2e8f0 !important;
|
||||||
padding: 0.45rem 0.65rem !important;
|
padding: 0.65rem 0.75rem !important;
|
||||||
vertical-align: middle !important;
|
vertical-align: middle !important;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
}
|
}
|
||||||
|
|
@ -489,28 +489,16 @@
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
font-size: 0.74rem !important;
|
font-size: 0.74rem !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
line-height: 1.3 !important;
|
line-height: 1.4 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rows & Cells */
|
/* Rows & Cells */
|
||||||
.bootstrap-table.bootstrap5 #unified-table td {
|
.bootstrap-table.bootstrap5 #unified-table td {
|
||||||
padding: 0.32rem 0.65rem !important;
|
padding: 0.65rem 0.75rem !important;
|
||||||
vertical-align: middle !important;
|
vertical-align: middle !important;
|
||||||
font-size: 0.82rem !important;
|
font-size: 0.83rem !important;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
border-bottom: 1px solid #f1f5f9 !important;
|
border-bottom: 1px solid #f1f5f9 !important;
|
||||||
line-height: 1.3 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-symbology-icon-img {
|
|
||||||
width: 20px !important;
|
|
||||||
height: 20px !important;
|
|
||||||
max-width: 20px !important;
|
|
||||||
max-height: 20px !important;
|
|
||||||
object-fit: contain !important;
|
|
||||||
vertical-align: middle !important;
|
|
||||||
display: inline-block !important;
|
|
||||||
margin-right: 6px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bootstrap-table.bootstrap5 #unified-table tbody tr {
|
.bootstrap-table.bootstrap5 #unified-table tbody tr {
|
||||||
|
|
@ -682,24 +670,24 @@
|
||||||
<div class="filter-section-header">
|
<div class="filter-section-header">
|
||||||
<label>{% translate "Type d'élément" %}</label>
|
<label>{% translate "Type d'élément" %}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-options d-flex gap-3 flex-wrap">
|
<div class="segmented-control">
|
||||||
<div class="filter-option">
|
<div class="filter-option segmented-option">
|
||||||
<input type="checkbox" name="filter-element-type" id="type-cb-assets" value="assets"
|
<input type="radio" name="unified-type-filter" id="type-all" value="all"
|
||||||
{% if preventive_element_type != 'locations' %}checked{% endif %}
|
{% if preventive_element_type != 'assets' and preventive_element_type != 'locations' %}checked{% endif %}
|
||||||
{% if preventive_element_type == 'locations' %}disabled{% endif %}>
|
|
||||||
<label for="type-cb-assets"><i class="bi bi-layers me-1 text-primary"></i>{% translate "Assets" %}</label>
|
|
||||||
</div>
|
|
||||||
<div class="filter-option">
|
|
||||||
<input type="checkbox" name="filter-element-type" id="type-cb-locations" value="locations"
|
|
||||||
{% if preventive_element_type != 'assets' %}checked{% endif %}
|
|
||||||
{% if preventive_element_type == 'assets' %}disabled{% endif %}>
|
|
||||||
<label for="type-cb-locations"><i class="bi bi-geo-alt me-1 text-info"></i>{% translate "Localisations" %}</label>
|
|
||||||
</div>
|
|
||||||
<div class="filter-option">
|
|
||||||
<input type="checkbox" name="filter-element-type" id="type-cb-management-zones" value="management_zones"
|
|
||||||
{% if not preventive_element_type %}checked{% endif %}
|
|
||||||
{% if preventive_element_type %}disabled{% endif %}>
|
{% if preventive_element_type %}disabled{% endif %}>
|
||||||
<label for="type-cb-management-zones"><i class="bi bi-diagram-3 me-1 text-success"></i>{% translate "Zones de gestion" %}</label>
|
<label for="type-all">{% translate "Assets & localisations" %}</label>
|
||||||
|
</div>
|
||||||
|
<div class="filter-option segmented-option">
|
||||||
|
<input type="radio" name="unified-type-filter" id="type-locations" value="locations"
|
||||||
|
{% if preventive_element_type == 'locations' %}checked{% endif %}
|
||||||
|
{% if preventive_element_type %}disabled{% endif %}>
|
||||||
|
<label for="type-locations">{% translate "Localisations uniquement" %}</label>
|
||||||
|
</div>
|
||||||
|
<div class="filter-option segmented-option">
|
||||||
|
<input type="radio" name="unified-type-filter" id="type-assets" value="assets"
|
||||||
|
{% if preventive_element_type == 'assets' %}checked{% endif %}
|
||||||
|
{% if preventive_element_type %}disabled{% endif %}>
|
||||||
|
<label for="type-assets">{% translate "Assets uniquement" %}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -849,7 +837,7 @@
|
||||||
{% translate "Choisissez d'abord le type d'élément, puis la thématique autorisée." %}
|
{% translate "Choisissez d'abord le type d'élément, puis la thématique autorisée." %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-6">
|
||||||
<div class="unified-add-panel h-100">
|
<div class="unified-add-panel h-100">
|
||||||
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-layers"></i>{% translate "Asset" %}</h6>
|
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-layers"></i>{% translate "Asset" %}</h6>
|
||||||
<label for="unified-add-asset-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
<label for="unified-add-asset-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
||||||
|
|
@ -859,7 +847,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-6">
|
||||||
<div class="unified-add-panel h-100">
|
<div class="unified-add-panel h-100">
|
||||||
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-geo-alt"></i>{% translate "Localisation" %}</h6>
|
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-geo-alt"></i>{% translate "Localisation" %}</h6>
|
||||||
<label for="unified-add-location-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
<label for="unified-add-location-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
||||||
|
|
@ -869,16 +857,6 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="unified-add-panel h-100">
|
|
||||||
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-diagram-3 text-success"></i>{% translate "Zone de gestion" %}</h6>
|
|
||||||
<label for="unified-add-management-zone-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
|
|
||||||
<select id="unified-add-management-zone-thematic" class="form-select mb-3"></select>
|
|
||||||
<button type="button" id="unified-add-management-zone-open" class="btn btn-outline-success w-100">
|
|
||||||
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer une zone" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="unified-add-chooser-empty" class="text-muted small mt-3 d-none">
|
<div id="unified-add-chooser-empty" class="text-muted small mt-3 d-none">
|
||||||
{% translate "Aucune thématique éditable n'est disponible pour la création." %}
|
{% translate "Aucune thématique éditable n'est disponible pour la création." %}
|
||||||
|
|
@ -1315,7 +1293,7 @@
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
||||||
<script src="{% static 'common/layer-switcher-control.js' %}?v=20261009_basemap"></script>
|
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
window.TERRITORY_MASK_URL = "{% url 'common:get_territory_mask_geojson' %}";
|
window.TERRITORY_MASK_URL = "{% url 'common:get_territory_mask_geojson' %}";
|
||||||
const preventive_draft_id = "{{ preventive_draft_id|default:'' }}";
|
const preventive_draft_id = "{{ preventive_draft_id|default:'' }}";
|
||||||
|
|
@ -1353,11 +1331,7 @@
|
||||||
trafficlightelectricalcabinet: "{% static 'assets/electrical-panel.png' %}",
|
trafficlightelectricalcabinet: "{% static 'assets/electrical-panel.png' %}",
|
||||||
publiclighting: "{% static 'assets/public_lamp_post_icon.svg' %}?v=20260521c",
|
publiclighting: "{% static 'assets/public_lamp_post_icon.svg' %}?v=20260521c",
|
||||||
publiclightingelectricalcabinet: "{% static 'assets/electrical-panel.png' %}",
|
publiclightingelectricalcabinet: "{% static 'assets/electrical-panel.png' %}",
|
||||||
nature: "{% static 'assets/nature_tree_icon.svg' %}?v=20261009_round",
|
nature: "{% static 'assets/nature_tree_icon.svg' %}?v=20260521a",
|
||||||
natureregional: "{% static 'assets/nature_tree_icon_regional.svg' %}?v=20261009_round",
|
|
||||||
natureremarkable: "{% static 'assets/nature_tree_icon_remarkable.svg' %}?v=20261009_round",
|
|
||||||
naturedead: "{% static 'assets/nature_tree_icon_dead.svg' %}?v=20261009_round",
|
|
||||||
naturegreensurface: "{% static 'assets/nature_greensurface_icon.svg' %}?v=20261010a",
|
|
||||||
structures: "{% static 'assets/structures_bridge_tunnel_icon.svg' %}?v=20260522b",
|
structures: "{% static 'assets/structures_bridge_tunnel_icon.svg' %}?v=20260522b",
|
||||||
roaddrain: "{% static 'assets/roaddrain_grille_icon.svg' %}?v=20260521a",
|
roaddrain: "{% static 'assets/roaddrain_grille_icon.svg' %}?v=20260521a",
|
||||||
speedbump: "{% static 'assets/road_speedbump_icon.svg' %}?v=20260920",
|
speedbump: "{% static 'assets/road_speedbump_icon.svg' %}?v=20260920",
|
||||||
|
|
@ -1374,7 +1348,7 @@
|
||||||
<script src="{% static 'common/base_script.js' %}"></script>
|
<script src="{% static 'common/base_script.js' %}"></script>
|
||||||
<script src="{% static 'common/base_map.js' %}"></script>
|
<script src="{% static 'common/base_map.js' %}"></script>
|
||||||
<script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script>
|
<script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script>
|
||||||
<script src="{% static 'assets/asset_symbology.js' %}?v=20261010_greensurface"></script>
|
<script src="{% static 'assets/asset_symbology.js' %}?v=20260920_grid_options"></script>
|
||||||
<script src="{% static 'assets/unified_map_layers.js' %}?v=20261010_greensurface"></script>
|
<script src="{% static 'assets/unified_map_layers.js' %}?v=20260920_grid_options"></script>
|
||||||
<script src="{% static 'assets/unified_index.js' %}?v=20261010_greensurface"></script>
|
<script src="{% static 'assets/unified_index.js' %}?v=20260920_grid_options"></script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
{% extends "assets/detail_generic.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block extra_blocks %}
|
|
||||||
<!-- Système de bornes parent -->
|
|
||||||
<div class="card mb-3" id="bollard-plot-system-card">
|
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-diagram-3-fill text-primary me-2"></i>{% translate "Système de bornes de rattachement" %}
|
|
||||||
</h5>
|
|
||||||
{% if asset.system %}
|
|
||||||
<a href="{{ asset.system.get_absolute_url }}" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir le système complet" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if asset.system %}
|
|
||||||
<div class="row align-items-center mb-3">
|
|
||||||
<div class="col-md-7">
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<span class="badge bg-dark rounded-pill fs-6 me-2">#{{ asset.position }}</span>
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<a href="{{ asset.system.get_absolute_url }}" class="text-decoration-none">
|
|
||||||
{{ asset.system.code }} - {{ asset.system.get_name|default:asset.system.code }}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small mb-2">
|
|
||||||
<i class="bi bi-geo-alt me-1"></i>{{ asset.system.location|default:"Localisation non définie" }}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-wrap gap-2">
|
|
||||||
{% if asset.system.width %}
|
|
||||||
<span class="badge bg-light text-dark border">
|
|
||||||
<i class="bi bi-arrows-expand me-1"></i>{% translate "Largeur" %} : {{ asset.system.width }} m
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if asset.system.direction %}
|
|
||||||
<span class="badge bg-light text-dark border">
|
|
||||||
<i class="bi bi-signpost-split me-1"></i>{{ asset.system.get_direction_display }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if asset.system.model %}
|
|
||||||
<span class="badge bg-light text-dark border">
|
|
||||||
<i class="bi bi-tag me-1"></i>{{ asset.system.model.get_name }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-5 text-md-end mt-3 mt-md-0">
|
|
||||||
<div class="p-3 bg-light rounded border text-start">
|
|
||||||
<div class="small text-muted mb-1">{% translate "Traverse complète" %} ({{ asset.system.plots.count }} {% translate "plots" %}) :</div>
|
|
||||||
<div class="d-flex flex-wrap gap-1">
|
|
||||||
{% for sibling in asset.system.plots.all %}
|
|
||||||
<a href="{{ sibling.get_absolute_url }}"
|
|
||||||
class="btn btn-sm {% if sibling.id == asset.id %}btn-primary fw-bold{% else %}btn-outline-secondary{% endif %}"
|
|
||||||
title="{{ sibling.code }} - {{ sibling.get_bollard_type_display }}">
|
|
||||||
#{{ sibling.position }} ({{ sibling.code }})
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-muted text-center p-3">
|
|
||||||
<i class="bi bi-exclamation-triangle text-warning me-2"></i>{% translate "Ce plot n'est actuellement rattaché à aucun système de bornes." %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Métadonnées techniques (extra_data) -->
|
|
||||||
{% if asset.extra_data %}
|
|
||||||
<div class="card mb-3" id="bollard-plot-extra-data-card">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-code-slash text-secondary me-2"></i>{% translate "Métadonnées techniques" %}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row mb-0">
|
|
||||||
{% for key, val in asset.extra_data.items %}
|
|
||||||
<dt class="col-sm-4 text-truncate" title="{{ key }}">{{ key }}</dt>
|
|
||||||
<dd class="col-sm-8 mb-2"><code>{{ val }}</code></dd>
|
|
||||||
{% endfor %}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock extra_blocks %}
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
{% extends "assets/detail_generic.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block extra_blocks %}
|
|
||||||
<!-- Plots de la traverse -->
|
|
||||||
<div class="card mb-3" id="bollard-system-plots-card">
|
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-geo-alt-fill text-primary me-2"></i>{% translate "Plots de la traverse" %}
|
|
||||||
<span class="badge bg-primary ms-2">{{ asset.plots.count }}</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
{% if asset.plots.all %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover table-striped mb-0 align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 80px;" class="text-center">{% translate "Pos." %}</th>
|
|
||||||
<th>{% translate "Code" %}</th>
|
|
||||||
<th>{% translate "Type de borne" %}</th>
|
|
||||||
<th>{% translate "Modèle" %}</th>
|
|
||||||
<th>{% translate "N° de série" %}</th>
|
|
||||||
<th>{% translate "Statut" %}</th>
|
|
||||||
<th style="width: 110px;" class="text-end">{% translate "Action" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for plot in asset.plots.all %}
|
|
||||||
<tr>
|
|
||||||
<td class="text-center">
|
|
||||||
<span class="badge bg-dark rounded-pill">#{{ plot.position }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ plot.get_absolute_url }}" class="fw-bold text-decoration-none">
|
|
||||||
{{ plot.code }}
|
|
||||||
</a>
|
|
||||||
{% if plot.get_name %}
|
|
||||||
<div class="small text-muted">{{ plot.get_name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if plot.bollard_type == 'automatic' %}
|
|
||||||
<span class="badge bg-primary"><i class="bi bi-arrow-up-down me-1"></i>{{ plot.get_bollard_type_display }}</span>
|
|
||||||
{% elif plot.bollard_type == 'semi_automatic' %}
|
|
||||||
<span class="badge bg-info text-dark">{{ plot.get_bollard_type_display }}</span>
|
|
||||||
{% elif plot.bollard_type == 'removable' %}
|
|
||||||
<span class="badge bg-warning text-dark">{{ plot.get_bollard_type_display }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">{{ plot.get_bollard_type_display }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ plot.model.get_name|default:"-" }}</td>
|
|
||||||
<td><code>{{ plot.serial_number|default:"-" }}</code></td>
|
|
||||||
<td>
|
|
||||||
{% if plot.status == 'active' %}
|
|
||||||
<span class="badge bg-success">{{ plot.get_status_display }}</span>
|
|
||||||
{% elif plot.status == 'damaged' %}
|
|
||||||
<span class="badge bg-warning text-dark">{{ plot.get_status_display }}</span>
|
|
||||||
{% elif plot.status == 'to_replace' %}
|
|
||||||
<span class="badge bg-danger">{{ plot.get_status_display }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">{{ plot.get_status_display }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<a href="{{ plot.get_absolute_url }}" class="btn btn-sm btn-outline-primary" title="{% translate 'Voir le détail du plot' %}">
|
|
||||||
<i class="bi bi-eye me-1"></i>{% translate "Voir" %}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="p-4 text-center text-muted">
|
|
||||||
<i class="bi bi-cone-striped display-6 d-block mb-2 text-secondary opacity-50"></i>
|
|
||||||
{% translate "Aucun plot physique n'est encore configuré sur ce système de bornes." %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Équipements associés au système (Caméras LAPI, totems, armoires...) -->
|
|
||||||
<div class="card mb-3" id="bollard-system-assets-card">
|
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-hdd-network text-info me-2"></i>{% translate "Équipements associés" %}
|
|
||||||
<span class="badge bg-secondary ms-2">{{ asset.system_assets.count }}</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
{% if asset.system_assets.all %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover table-striped mb-0 align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Rôle" %}</th>
|
|
||||||
<th>{% translate "Équipement" %}</th>
|
|
||||||
<th>{% translate "Modèle" %}</th>
|
|
||||||
<th>{% translate "Statut" %}</th>
|
|
||||||
<th>{% translate "Commentaire" %}</th>
|
|
||||||
<th style="width: 110px;" class="text-end">{% translate "Action" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for item in asset.system_assets.all %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if 'camera' in item.role %}
|
|
||||||
<span class="badge bg-info text-dark"><i class="bi bi-camera-video me-1"></i>{{ item.get_role_display }}</span>
|
|
||||||
{% elif item.role == 'cabinet' %}
|
|
||||||
<span class="badge bg-dark"><i class="bi bi-cpu me-1"></i>{{ item.get_role_display }}</span>
|
|
||||||
{% elif item.role == 'totem' %}
|
|
||||||
<span class="badge bg-primary"><i class="bi bi-tablet me-1"></i>{{ item.get_role_display }}</span>
|
|
||||||
{% elif item.role == 'traffic_light' %}
|
|
||||||
<span class="badge bg-warning text-dark"><i class="bi bi-traffic-light me-1"></i>{{ item.get_role_display }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">{{ item.get_role_display }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ item.asset.get_absolute_url }}" class="fw-bold text-decoration-none">
|
|
||||||
{{ item.asset.code }}
|
|
||||||
</a>
|
|
||||||
{% if item.asset.get_name %}
|
|
||||||
<div class="small text-muted">{{ item.asset.get_name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ item.asset.model.get_name|default:"-" }}</td>
|
|
||||||
<td>
|
|
||||||
{% if item.asset.status == 'active' %}
|
|
||||||
<span class="badge bg-success">{{ item.asset.get_status_display }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">{{ item.asset.get_status_display }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ item.comment|default:"-" }}</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<a href="{{ item.asset.get_absolute_url }}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Voir équipement' %}">
|
|
||||||
<i class="bi bi-box-arrow-up-right"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="p-4 text-center text-muted">
|
|
||||||
<i class="bi bi-camera-video display-6 d-block mb-2 text-secondary opacity-50"></i>
|
|
||||||
{% translate "Aucun équipement périphérique (caméra, armoire, totem...) n'est actuellement associé." %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contrats de maintenance liés -->
|
|
||||||
{% if asset.bollard_systems_contracts.exists %}
|
|
||||||
<div class="card mb-3" id="bollard-system-contracts-card">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-file-earmark-text text-secondary me-2"></i>{% translate "Contrats d'entretien" %}
|
|
||||||
<span class="badge bg-secondary ms-2">{{ asset.bollard_systems_contracts.count }}</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover table-striped mb-0 align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% translate "Numéro de contrat" %}</th>
|
|
||||||
<th>{% translate "Type de maintenance" %}</th>
|
|
||||||
<th>{% translate "Statut" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for sc in asset.bollard_systems_contracts.all %}
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">{{ sc.contract.contract_number }} - {{ sc.contract.name }}</td>
|
|
||||||
<td>{{ sc.get_maintenance_type_display }}</td>
|
|
||||||
<td>
|
|
||||||
{% if sc.status == 'active' %}
|
|
||||||
<span class="badge bg-success">{% translate "Actif" %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">{{ sc.get_status_display }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Métadonnées techniques (extra_data) -->
|
|
||||||
{% if asset.extra_data %}
|
|
||||||
<div class="card mb-3" id="bollard-system-extra-data-card">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-code-slash text-secondary me-2"></i>{% translate "Métadonnées techniques" %}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row mb-0">
|
|
||||||
{% for key, val in asset.extra_data.items %}
|
|
||||||
<dt class="col-sm-4 text-truncate" title="{{ key }}">{{ key }}</dt>
|
|
||||||
<dd class="col-sm-8 mb-2"><code>{{ val }}</code></dd>
|
|
||||||
{% endfor %}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock extra_blocks %}
|
|
||||||
|
|
@ -103,11 +103,11 @@ def get_asset_specific_fields(asset):
|
||||||
from django.contrib.gis.db.models import GeometryField
|
from django.contrib.gis.db.models import GeometryField
|
||||||
from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField,
|
from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField,
|
||||||
BooleanField, FloatField, DecimalField, IntegerField,
|
BooleanField, FloatField, DecimalField, IntegerField,
|
||||||
DateField, DateTimeField, TextField, JSONField)
|
DateField, DateTimeField, TextField)
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from assets.models import AbstractAsset, AbstractGeoAsset
|
from assets.models import AbstractAsset, AbstractGeoAsset
|
||||||
|
|
||||||
excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField, JSONField)
|
excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField)
|
||||||
|
|
||||||
base_field_names = {f.name for f in AbstractAsset._meta.fields}
|
base_field_names = {f.name for f in AbstractAsset._meta.fields}
|
||||||
base_field_names |= {f.name for f in AbstractGeoAsset._meta.fields}
|
base_field_names |= {f.name for f in AbstractGeoAsset._meta.fields}
|
||||||
|
|
|
||||||
1066
loko/assets/tests.py
1066
loko/assets/tests.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,292 +0,0 @@
|
||||||
import json
|
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
from datetime import date, datetime
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.core.management import call_command
|
|
||||||
from django.contrib.gis.geos import Point, Polygon, MultiPolygon
|
|
||||||
|
|
||||||
from common.models import Thematic
|
|
||||||
from assets.models import NatureTree, AssetCategory
|
|
||||||
from contracts.models import Contract, ContractOrder, Company
|
|
||||||
from interventions.models import Intervention, InterventionAsset
|
|
||||||
|
|
||||||
|
|
||||||
class ImportFulcrumTreesCommandTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.thematic = Thematic.objects.create(name_fr="Nature", name_nl="Natuur")
|
|
||||||
self.category = AssetCategory.objects.create(id=60, name_fr="Arbres", name_nl="Bomen")
|
|
||||||
self.company = Company.objects.create(name="Krinkels")
|
|
||||||
self.contract = Contract.objects.create(
|
|
||||||
company=self.company,
|
|
||||||
contract_number="E19.051",
|
|
||||||
description="Marché E19.051",
|
|
||||||
start_date=date(2020, 1, 1),
|
|
||||||
end_date=date(2030, 12, 31)
|
|
||||||
)
|
|
||||||
self.contract.thematics.add(self.thematic)
|
|
||||||
|
|
||||||
def test_import_fulcrum_trees_and_interventions(self):
|
|
||||||
# 1. Préparer gestion.csv temporaire
|
|
||||||
gestion_content = (
|
|
||||||
"com_id,com_typ,com_nom,_status,com_dat,com_ech\n"
|
|
||||||
"BC-TEST-001,Marché E19.051 - Diagnostic,Élagage test,commande,2024-05-10,2024-06-15\n"
|
|
||||||
)
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f_ges:
|
|
||||||
f_ges.write(gestion_content)
|
|
||||||
ges_path = f_ges.name
|
|
||||||
|
|
||||||
# 2. Préparer arbes.geojson temporaire
|
|
||||||
mock_tree_uuid = "11111111-2222-3333-4444-555555555555"
|
|
||||||
tree_geojson = {
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Point",
|
|
||||||
"coordinates": [4.3517, 50.8503]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"_record_id": mock_tree_uuid,
|
|
||||||
"id_sit_pos": "099.01.001",
|
|
||||||
"id_visuel": "V-9901",
|
|
||||||
"sit_id": "099.01",
|
|
||||||
"pos_id": "001",
|
|
||||||
"sit_nom": "Boulevard du Test",
|
|
||||||
"comm_commune": "Bruxelles",
|
|
||||||
"arb_ess": "Platanus x hispanica",
|
|
||||||
"arb_vern": "Platane commun",
|
|
||||||
"arb_tax": "Platanus hispanica",
|
|
||||||
"arb_haut": 18.5,
|
|
||||||
"arb_circ": 140.0,
|
|
||||||
"arb_diam_tmp": 8.0,
|
|
||||||
"arb_st": "V",
|
|
||||||
"arb_vital": "Bo",
|
|
||||||
"arb_ont": "Ad",
|
|
||||||
"ges_conduite": "LI",
|
|
||||||
"cf_sit": 4.5,
|
|
||||||
"arb_anpla": "1995",
|
|
||||||
"maj_date": "2024-05-10T00:00:00.000Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.geojson', delete=False) as f_arb:
|
|
||||||
json.dump(tree_geojson, f_arb)
|
|
||||||
arb_path = f_arb.name
|
|
||||||
|
|
||||||
# 3. Préparer interventions.geojson temporaire
|
|
||||||
mock_itv_uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
||||||
itv_geojson = {
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Point",
|
|
||||||
"coordinates": [4.3517, 50.8503]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"_record_id": mock_itv_uuid,
|
|
||||||
"_title": "099.01.001, Formation, Dans 1 an",
|
|
||||||
"rec_interv_com": "Taille branches basses",
|
|
||||||
"sit_nom": "Boulevard du Test",
|
|
||||||
"comm_commune": "Bruxelles",
|
|
||||||
"sit_id": "099.01",
|
|
||||||
"pos_id": "001",
|
|
||||||
"dia_comma": "BC-TEST-001",
|
|
||||||
"dia_date": "2024-05-10T00:00:00.000Z",
|
|
||||||
"in_deadline": "2025-05-10T00:00:00.000Z",
|
|
||||||
"rec_delai": "Pla-1",
|
|
||||||
"rec_interv": "FO",
|
|
||||||
"in_fin_yn": "no",
|
|
||||||
"mater_arbres_lk": [mock_tree_uuid]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.geojson', delete=False) as f_itv:
|
|
||||||
json.dump(itv_geojson, f_itv)
|
|
||||||
itv_path = f_itv.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Exécuter la commande
|
|
||||||
call_command(
|
|
||||||
'import_fulcrum_trees',
|
|
||||||
arbes_file=arb_path,
|
|
||||||
gestion_file=ges_path,
|
|
||||||
interventions_file=itv_path,
|
|
||||||
managing_authority="Test Authority",
|
|
||||||
management_type="regional",
|
|
||||||
code_prefix="TEST-ARB-"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Vérifications gestion / bon de commande
|
|
||||||
order = ContractOrder.objects.filter(order_code="BC-TEST-001").first()
|
|
||||||
self.assertIsNotNone(order)
|
|
||||||
self.assertEqual(order.contract.contract_number, "E19.051")
|
|
||||||
self.assertEqual(order.order_status, "sent")
|
|
||||||
|
|
||||||
# Vérifications arbre
|
|
||||||
tree = NatureTree.objects.filter(external_reference=mock_tree_uuid).first()
|
|
||||||
self.assertIsNotNone(tree)
|
|
||||||
self.assertEqual(tree.code, "TEST-ARB-099.01.001")
|
|
||||||
self.assertEqual(tree.managing_authority, "Test Authority")
|
|
||||||
self.assertEqual(tree.management_type, "regional")
|
|
||||||
self.assertEqual(tree.circumference, 140.0)
|
|
||||||
self.assertEqual(tree.height, 18.5)
|
|
||||||
self.assertIsNotNone(tree.amenity_value)
|
|
||||||
self.assertGreater(tree.amenity_value, 0)
|
|
||||||
self.assertIsNotNone(tree.carbon_stock)
|
|
||||||
self.assertGreater(tree.carbon_stock, 0)
|
|
||||||
self.assertIsNotNone(tree.cooling_indicator)
|
|
||||||
|
|
||||||
# Vérifications intervention
|
|
||||||
itv = Intervention.objects.filter(source_ref=mock_itv_uuid).first()
|
|
||||||
self.assertIsNotNone(itv)
|
|
||||||
self.assertEqual(itv.order_id, order.id)
|
|
||||||
self.assertEqual(itv.priority, "3") # Pla-1 -> 3
|
|
||||||
self.assertEqual(itv.status, "to_be_planned")
|
|
||||||
self.assertIsNotNone(itv.expected_begin_time)
|
|
||||||
self.assertEqual(itv.expected_begin_time.date(), date(2024, 5, 10))
|
|
||||||
self.assertIsNotNone(itv.expected_end_time)
|
|
||||||
self.assertEqual(itv.expected_end_time.date(), date(2025, 5, 10))
|
|
||||||
|
|
||||||
# Vérifications dates de maintenance sur l'arbre
|
|
||||||
self.assertEqual(tree.last_phytosanitary_date, date(2024, 5, 10))
|
|
||||||
self.assertEqual(tree.next_inspection_date, date(2025, 5, 10))
|
|
||||||
self.assertEqual(tree.pruning_frequency_years, 1)
|
|
||||||
|
|
||||||
# Vérifications événements de planification (timeline)
|
|
||||||
self.assertEqual(itv.planned_events.count(), 2)
|
|
||||||
begin_event = itv.planned_events.filter(event_type='expected_begin').first()
|
|
||||||
self.assertIsNotNone(begin_event)
|
|
||||||
self.assertEqual(begin_event.event_time.date(), date(2024, 5, 10))
|
|
||||||
end_event = itv.planned_events.filter(event_type='expected_end').first()
|
|
||||||
self.assertIsNotNone(end_event)
|
|
||||||
self.assertEqual(end_event.event_time.date(), date(2025, 5, 10))
|
|
||||||
|
|
||||||
# Vérification liaison InterventionAsset
|
|
||||||
links = InterventionAsset.objects.filter(intervention=itv)
|
|
||||||
self.assertEqual(links.count(), 1)
|
|
||||||
self.assertEqual(links.first().object_id, tree.id)
|
|
||||||
|
|
||||||
# Test mise à jour sans duplication
|
|
||||||
tree_geojson['features'][0]['properties']['arb_circ'] = 145.0
|
|
||||||
with open(arb_path, 'w') as f_arb_update:
|
|
||||||
json.dump(tree_geojson, f_arb_update)
|
|
||||||
|
|
||||||
call_command(
|
|
||||||
'import_fulcrum_trees',
|
|
||||||
arbes_file=arb_path,
|
|
||||||
gestion_file=ges_path,
|
|
||||||
interventions_file=itv_path,
|
|
||||||
skip_gestion=True,
|
|
||||||
skip_interventions=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Le nombre d'arbres ne doit pas avoir augmenté
|
|
||||||
self.assertEqual(NatureTree.objects.filter(external_reference=mock_tree_uuid).count(), 1)
|
|
||||||
tree.refresh_from_db()
|
|
||||||
self.assertEqual(tree.circumference, 145.0)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(ges_path): os.remove(ges_path)
|
|
||||||
if os.path.exists(arb_path): os.remove(arb_path)
|
|
||||||
if os.path.exists(itv_path): os.remove(itv_path)
|
|
||||||
|
|
||||||
def test_import_fulcrum_regional_trees_alias(self):
|
|
||||||
from io import StringIO
|
|
||||||
out = StringIO()
|
|
||||||
call_command('import_fulcrum_regional_trees', skip_trees=True, skip_gestion=True, skip_interventions=True, stdout=out)
|
|
||||||
self.assertIn("FIN DE L'IMPORTATION", out.getvalue())
|
|
||||||
|
|
||||||
def test_attach_trees_to_green_sections(self):
|
|
||||||
from assets.models import NatureLocation
|
|
||||||
from django.contrib.gis.geos import Polygon, MultiPolygon
|
|
||||||
# Créer une section verte polygonale
|
|
||||||
poly = Polygon(((648000, 670000), (648100, 670000), (648100, 670100), (648000, 670100), (648000, 670000)), srid=3812)
|
|
||||||
loc = NatureLocation.objects.create(
|
|
||||||
code="WAT_R22_008",
|
|
||||||
name_fr="Boulevard du Souverain",
|
|
||||||
geom=MultiPolygon(poly, srid=3812)
|
|
||||||
)
|
|
||||||
# Créer un arbre à l'intérieur du polygone (sans location)
|
|
||||||
pt = Point(648050, 670050, srid=3812)
|
|
||||||
pt_4326 = pt.clone().transform(4326, clone=True)
|
|
||||||
tree = NatureTree.objects.create(
|
|
||||||
code="TR-ATTACH-TEST",
|
|
||||||
name_fr="Arbre test Souverain",
|
|
||||||
category=self.category,
|
|
||||||
geom=pt,
|
|
||||||
lon=pt_4326.x,
|
|
||||||
lat=pt_4326.y,
|
|
||||||
location=None
|
|
||||||
)
|
|
||||||
self.assertEqual(loc.nature_trees.count(), 0)
|
|
||||||
|
|
||||||
# Exécuter le rattachement
|
|
||||||
call_command('attach_trees_to_green_sections', max_distance=25, skip_sync_munis=True)
|
|
||||||
|
|
||||||
tree.refresh_from_db()
|
|
||||||
self.assertEqual(tree.location_id, loc.id)
|
|
||||||
self.assertEqual(loc.nature_trees.count(), 1)
|
|
||||||
|
|
||||||
def test_cancel_obsolete_interventions_and_theoretical_inspection(self):
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from interventions.models import Intervention, InterventionAsset, InterventionTimeLine
|
|
||||||
|
|
||||||
# Créer un arbre avec dernier diagnostic en 2025 et périodicité 1 an
|
|
||||||
tree = NatureTree.objects.create(
|
|
||||||
code="TR-OBSOLETE-TEST",
|
|
||||||
name_fr="Arbre test obsolète",
|
|
||||||
category=self.category,
|
|
||||||
last_phytosanitary_date=date(2025, 12, 4),
|
|
||||||
pruning_frequency_years=1,
|
|
||||||
next_inspection_date=date(2023, 7, 22),
|
|
||||||
geom=Point(648050, 670050, srid=3812),
|
|
||||||
lon=4.42,
|
|
||||||
lat=50.80
|
|
||||||
)
|
|
||||||
|
|
||||||
# Créer une intervention en attente échue en 2023 (antérieure au diagnostic de 2025)
|
|
||||||
itv = Intervention.objects.create(
|
|
||||||
code="I999999",
|
|
||||||
title="003.01.270.(1912).00, Entretien, Dans 1 an",
|
|
||||||
status="to_be_planned",
|
|
||||||
status_order=5,
|
|
||||||
expected_begin_time=datetime(2022, 7, 22, 0, 0),
|
|
||||||
expected_end_time=datetime(2023, 7, 22, 0, 0),
|
|
||||||
thematic=self.thematic,
|
|
||||||
geom=MultiPolygon(Polygon(((648000, 670000), (648010, 670000), (648010, 670010), (648000, 670010), (648000, 670000)), srid=3812))
|
|
||||||
)
|
|
||||||
ct_tree = ContentType.objects.get_for_model(NatureTree)
|
|
||||||
InterventionAsset.objects.create(
|
|
||||||
intervention=itv,
|
|
||||||
content_type=ct_tree,
|
|
||||||
object_id=tree.id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Exécuter la synchronisation des dates de maintenance
|
|
||||||
call_command('import_fulcrum_trees', sync_tree_dates=True)
|
|
||||||
|
|
||||||
# Vérifier que l'intervention échue a été annulée
|
|
||||||
itv.refresh_from_db()
|
|
||||||
self.assertEqual(itv.status, "canceled")
|
|
||||||
self.assertEqual(itv.status_order, 200)
|
|
||||||
self.assertEqual(itv.cancellation_type, "non_relevant")
|
|
||||||
|
|
||||||
# Vérifier que l'événement d'annulation a été consigné dans la timeline
|
|
||||||
tl = InterventionTimeLine.objects.filter(intervention=itv, to_status="canceled").first()
|
|
||||||
self.assertIsNotNone(tl)
|
|
||||||
self.assertIn("caduque", tl.event_description)
|
|
||||||
self.assertIn("04/12/2025", tl.event_description)
|
|
||||||
|
|
||||||
# Vérifier que la prochaine inspection de l'arbre est calculée théoriquement : 2025-12-04 + 1 an = 2026-12-04
|
|
||||||
tree.refresh_from_db()
|
|
||||||
self.assertEqual(tree.next_inspection_date, date(2026, 12, 4))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,597 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.contrib.gis.geos import Polygon, MultiPolygon, Point
|
|
||||||
|
|
||||||
from common.models import Thematic, Role, UserConfig, UserThematics
|
|
||||||
from assets.models import (
|
|
||||||
ManagementZoneType,
|
|
||||||
ManagementZone,
|
|
||||||
ManagementZoneAsset,
|
|
||||||
NatureGreenSurface,
|
|
||||||
NatureLocation,
|
|
||||||
)
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneModelTests(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.thematic_nature = Thematic.objects.create(
|
|
||||||
code="nature",
|
|
||||||
name_fr="Nature",
|
|
||||||
name_nl="Natuur"
|
|
||||||
)
|
|
||||||
self.zone_type = ManagementZoneType.objects.create(
|
|
||||||
code="fauche_tardive",
|
|
||||||
name_fr="Fauche tardive",
|
|
||||||
name_nl="Laat maaien",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
default_color="#28a745",
|
|
||||||
description="Zone de gestion écologique avec fauche annuelle en automne."
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_management_zone_type_bilingual_and_str(self):
|
|
||||||
self.assertEqual(self.zone_type.name_fr, "Fauche tardive")
|
|
||||||
self.assertEqual(self.zone_type.name_nl, "Laat maaien")
|
|
||||||
self.assertIn("Fauche tardive", str(self.zone_type))
|
|
||||||
|
|
||||||
def test_management_zone_creation_geometry_and_color_fallback(self):
|
|
||||||
poly = Polygon(((0, 0), (100, 0), (100, 100), (0, 100), (0, 0)))
|
|
||||||
multi_poly = MultiPolygon([poly], srid=3812)
|
|
||||||
|
|
||||||
# 1. Sans couleur explicite -> prend la couleur par défaut du type
|
|
||||||
zone1 = ManagementZone.objects.create(
|
|
||||||
code="ZG-NAT-001",
|
|
||||||
name_fr="Marais de Jette - Prairie",
|
|
||||||
name_nl="Moeras van Jette - Weide",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
zone_type=self.zone_type,
|
|
||||||
geom=multi_poly,
|
|
||||||
target_frequency_days=365,
|
|
||||||
seasonality_notes="Fauche en septembre; pas de taille au printemps",
|
|
||||||
instructions="Attention aux zones humides et aux ornières.",
|
|
||||||
)
|
|
||||||
self.assertEqual(zone1.get_color(), "#28a745")
|
|
||||||
self.assertEqual(zone1.geom.srid, 3812)
|
|
||||||
self.assertEqual(zone1.geom.geom_type, "MultiPolygon")
|
|
||||||
|
|
||||||
# GeoJSON property
|
|
||||||
geojson = zone1.geojson_geometry
|
|
||||||
self.assertIsNotNone(geojson)
|
|
||||||
self.assertEqual(geojson["type"], "MultiPolygon")
|
|
||||||
|
|
||||||
# Spatial intersection
|
|
||||||
pt_inside = Point(50, 50, srid=3812)
|
|
||||||
pt_outside = Point(200, 200, srid=3812)
|
|
||||||
self.assertTrue(ManagementZone.objects.filter(pk=zone1.pk, geom__intersects=pt_inside).exists())
|
|
||||||
self.assertFalse(ManagementZone.objects.filter(pk=zone1.pk, geom__intersects=pt_outside).exists())
|
|
||||||
|
|
||||||
# 2. Avec couleur personnalisée explicite
|
|
||||||
zone2 = ManagementZone.objects.create(
|
|
||||||
code="ZG-NAT-002",
|
|
||||||
name_fr="Zone spéciale",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
color="#ff5722",
|
|
||||||
)
|
|
||||||
self.assertEqual(zone2.get_color(), "#ff5722")
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneAssetTests(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
|
|
||||||
self.zone = ManagementZone.objects.create(
|
|
||||||
code="ZG-NAT-TEST",
|
|
||||||
name_fr="Zone test",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
)
|
|
||||||
self.location = NatureLocation.objects.create(code="LOC-01", name_fr="Parc de Jette")
|
|
||||||
self.green_surface = NatureGreenSurface.objects.create(
|
|
||||||
code="GS-01",
|
|
||||||
name_fr="Pelouse 1",
|
|
||||||
location=self.location,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_explicit_asset_association_and_exclusion(self):
|
|
||||||
# Association explicite d'inclusion
|
|
||||||
link = ManagementZoneAsset.objects.create(
|
|
||||||
management_zone=self.zone,
|
|
||||||
asset=self.green_surface,
|
|
||||||
is_excluded=False,
|
|
||||||
notes="Inclus dans le plan"
|
|
||||||
)
|
|
||||||
self.assertIn("Inclus", str(link))
|
|
||||||
self.assertEqual(self.zone.explicit_assets.count(), 1)
|
|
||||||
self.assertEqual(self.zone.explicit_assets.first().asset, self.green_surface)
|
|
||||||
|
|
||||||
# Bascule en exclusion
|
|
||||||
link.is_excluded = True
|
|
||||||
link.save()
|
|
||||||
self.assertIn("Exclu", str(link))
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneGeojsonEndpointTests(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.user = User.objects.create_user(username="nature_manager", password="password")
|
|
||||||
self.role_manager = Role.objects.create(name="manager")
|
|
||||||
self.config = UserConfig.objects.create(user=self.user)
|
|
||||||
self.config.roles.add(self.role_manager)
|
|
||||||
|
|
||||||
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
|
|
||||||
self.thematic_roads = Thematic.objects.create(code="roads", name_fr="Voiries", name_nl="Wegen")
|
|
||||||
|
|
||||||
UserThematics.objects.create(
|
|
||||||
user_config=self.config,
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
can_view_assets=True,
|
|
||||||
can_edit_assets=True
|
|
||||||
)
|
|
||||||
|
|
||||||
self.zone_type = ManagementZoneType.objects.create(
|
|
||||||
code="fauche",
|
|
||||||
name_fr="Fauche tardive",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
default_color="#198754"
|
|
||||||
)
|
|
||||||
|
|
||||||
poly = Polygon(((0, 0), (10, 0), (10, 10), (0, 10), (0, 0)))
|
|
||||||
self.multi_poly = MultiPolygon([poly], srid=3812)
|
|
||||||
|
|
||||||
self.zone_nature = ManagementZone.objects.create(
|
|
||||||
code="ZG-NAT-1",
|
|
||||||
name_fr="Prairie fleurie",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
zone_type=self.zone_type,
|
|
||||||
geom=self.multi_poly,
|
|
||||||
instructions="Ne pas tondre au printemps",
|
|
||||||
target_frequency_days=180,
|
|
||||||
seasonality_notes="Septembre",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.zone_roads = ManagementZone.objects.create(
|
|
||||||
code="ZG-ROAD-1",
|
|
||||||
name_fr="Itinéraire déneigement P1",
|
|
||||||
thematic=self.thematic_roads,
|
|
||||||
geom=self.multi_poly,
|
|
||||||
color="#0d6efd",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_anonymous_access_redirects(self):
|
|
||||||
url = reverse("assets:management_zones_geojson")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
|
|
||||||
def test_authenticated_user_access_nature_zones(self):
|
|
||||||
self.client.login(username="nature_manager", password="password")
|
|
||||||
url = reverse("assets:management_zones_geojson") + "?thematic=nature"
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
self.assertEqual(data["type"], "FeatureCollection")
|
|
||||||
self.assertEqual(len(data["features"]), 1)
|
|
||||||
|
|
||||||
feat = data["features"][0]
|
|
||||||
self.assertEqual(feat["id"], self.zone_nature.id)
|
|
||||||
self.assertEqual(feat["properties"]["code"], "ZG-NAT-1")
|
|
||||||
self.assertEqual(feat["properties"]["name"], "Prairie fleurie")
|
|
||||||
self.assertEqual(feat["properties"]["thematic"], "nature")
|
|
||||||
self.assertEqual(feat["properties"]["zone_type_name"], "Fauche tardive")
|
|
||||||
self.assertEqual(feat["properties"]["color"], "#198754")
|
|
||||||
self.assertEqual(feat["properties"]["instructions"], "Ne pas tondre au printemps")
|
|
||||||
self.assertEqual(feat["properties"]["target_frequency_days"], 180)
|
|
||||||
self.assertEqual(feat["geometry"]["type"], "MultiPolygon")
|
|
||||||
|
|
||||||
def test_user_without_thematic_permission_denied(self):
|
|
||||||
self.client.login(username="nature_manager", password="password")
|
|
||||||
# nature_manager n'a pas accès à la thématique 'roads'
|
|
||||||
url = reverse("assets:management_zones_geojson") + "?thematic=roads"
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
def test_superuser_access_all_thematics(self):
|
|
||||||
admin = User.objects.create_superuser(username="superadmin", password="password", email="admin@example.com")
|
|
||||||
self.client.login(username="superadmin", password="password")
|
|
||||||
|
|
||||||
# Sans paramètre thématique, superuser reçoit toutes les zones avec géométrie
|
|
||||||
url = reverse("assets:management_zones_geojson")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = response.json()
|
|
||||||
self.assertEqual(len(data["features"]), 2)
|
|
||||||
|
|
||||||
|
|
||||||
import json
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
||||||
from assets.permissions import (
|
|
||||||
can_view_management_zones,
|
|
||||||
can_edit_management_zones,
|
|
||||||
can_delete_management_zone,
|
|
||||||
get_editable_thematics_for_management_zones,
|
|
||||||
)
|
|
||||||
from assets.forms import ManagementZoneForm
|
|
||||||
from assets.models import ManagementZoneDocument
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZonesPermissionAndCRUDTests(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# Users
|
|
||||||
self.user_manager = User.objects.create_user(username="mgr", password="password")
|
|
||||||
self.user_viewer = User.objects.create_user(username="viewer", password="password")
|
|
||||||
self.user_editor = User.objects.create_user(username="editor", password="password")
|
|
||||||
self.user_no_access = User.objects.create_user(username="no_access", password="password")
|
|
||||||
|
|
||||||
# Thematics
|
|
||||||
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
|
|
||||||
self.thematic_roads = Thematic.objects.create(code="roads", name_fr="Voiries", name_nl="Wegen")
|
|
||||||
|
|
||||||
# Roles & Configs
|
|
||||||
self.role_manager = Role.objects.create(name="manager")
|
|
||||||
self.role_operator = Role.objects.create(name="operator")
|
|
||||||
|
|
||||||
self.config_mgr = UserConfig.objects.create(user=self.user_manager)
|
|
||||||
self.config_mgr.roles.add(self.role_manager)
|
|
||||||
|
|
||||||
self.config_viewer = UserConfig.objects.create(user=self.user_viewer)
|
|
||||||
UserThematics.objects.create(
|
|
||||||
user_config=self.config_viewer,
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
can_view_assets=True,
|
|
||||||
can_edit_assets=False,
|
|
||||||
can_edit_management_zones=False
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config_editor = UserConfig.objects.create(user=self.user_editor)
|
|
||||||
UserThematics.objects.create(
|
|
||||||
user_config=self.config_editor,
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
can_view_assets=True,
|
|
||||||
can_edit_assets=False,
|
|
||||||
can_edit_management_zones=True # Specific permission
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config_no_access = UserConfig.objects.create(user=self.user_no_access)
|
|
||||||
|
|
||||||
# Type de zone
|
|
||||||
self.zone_type = ManagementZoneType.objects.create(
|
|
||||||
code="fauche_eco",
|
|
||||||
name_fr="Fauche écologique",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
default_color="#20c997"
|
|
||||||
)
|
|
||||||
|
|
||||||
poly = Polygon(((0, 0), (50, 0), (50, 50), (0, 50), (0, 0)))
|
|
||||||
self.zone = ManagementZone.objects.create(
|
|
||||||
code="ZG-TEST-01",
|
|
||||||
name_fr="Zone Test Marais",
|
|
||||||
name_nl="Testzone Moeras",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
zone_type=self.zone_type,
|
|
||||||
geom=MultiPolygon([poly], srid=3812),
|
|
||||||
target_frequency_days=90,
|
|
||||||
seasonality_notes="Avril-Septembre",
|
|
||||||
instructions="Attention à la faune aquatique",
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_permission_functions(self):
|
|
||||||
# Manager can view and edit any thematic
|
|
||||||
self.assertTrue(can_view_management_zones(self.user_manager))
|
|
||||||
self.assertTrue(can_edit_management_zones(self.user_manager))
|
|
||||||
self.assertTrue(can_edit_management_zones(self.user_manager, thematic=self.thematic_nature))
|
|
||||||
|
|
||||||
# Viewer can view nature, but cannot edit
|
|
||||||
self.assertTrue(can_view_management_zones(self.user_viewer, thematic=self.thematic_nature))
|
|
||||||
self.assertFalse(can_view_management_zones(self.user_viewer, thematic=self.thematic_roads))
|
|
||||||
self.assertFalse(can_edit_management_zones(self.user_viewer))
|
|
||||||
self.assertFalse(can_edit_management_zones(self.user_viewer, thematic=self.thematic_nature))
|
|
||||||
|
|
||||||
# Editor with can_edit_management_zones=True
|
|
||||||
self.assertTrue(can_view_management_zones(self.user_editor, thematic=self.thematic_nature))
|
|
||||||
self.assertTrue(can_edit_management_zones(self.user_editor))
|
|
||||||
self.assertTrue(can_edit_management_zones(self.user_editor, thematic=self.thematic_nature))
|
|
||||||
self.assertFalse(can_edit_management_zones(self.user_editor, thematic=self.thematic_roads))
|
|
||||||
|
|
||||||
# Editable thematics
|
|
||||||
editable = get_editable_thematics_for_management_zones(self.user_editor)
|
|
||||||
self.assertIn(self.thematic_nature, editable)
|
|
||||||
self.assertNotIn(self.thematic_roads, editable)
|
|
||||||
|
|
||||||
def test_management_zone_form_with_geom_geojson(self):
|
|
||||||
# Test initial geojson
|
|
||||||
form = ManagementZoneForm(instance=self.zone, user=self.user_editor)
|
|
||||||
self.assertIn('geom_geojson', form.initial)
|
|
||||||
geom_dict = json.loads(form.initial['geom_geojson'])
|
|
||||||
self.assertEqual(geom_dict['type'], 'MultiPolygon')
|
|
||||||
|
|
||||||
# Test saving with new GeoJSON polygon in WGS84
|
|
||||||
geojson_polygon = {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [[[4.35, 50.84], [4.36, 50.84], [4.36, 50.85], [4.35, 50.85], [4.35, 50.84]]]
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
'code': 'ZG-NEW-01',
|
|
||||||
'name_fr': 'Nouvelle zone',
|
|
||||||
'thematic': self.thematic_nature.pk,
|
|
||||||
'zone_type': self.zone_type.pk,
|
|
||||||
'color': '#ff9900',
|
|
||||||
'target_frequency_days': 30,
|
|
||||||
'geom_geojson': json.dumps(geojson_polygon),
|
|
||||||
'is_active': True,
|
|
||||||
}
|
|
||||||
form = ManagementZoneForm(data=data, user=self.user_editor)
|
|
||||||
self.assertTrue(form.is_valid(), form.errors)
|
|
||||||
new_zone = form.save()
|
|
||||||
self.assertEqual(new_zone.code, 'ZG-NEW-01')
|
|
||||||
self.assertIsNotNone(new_zone.geom)
|
|
||||||
self.assertEqual(new_zone.geom.srid, 3812)
|
|
||||||
self.assertEqual(new_zone.geom.geom_type, 'MultiPolygon')
|
|
||||||
|
|
||||||
def test_management_zones_index_view(self):
|
|
||||||
url = reverse("assets:management_zones_index")
|
|
||||||
|
|
||||||
# Anonymous -> 302
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
|
|
||||||
# No access user -> 403
|
|
||||||
self.client.login(username="no_access", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 403)
|
|
||||||
|
|
||||||
# Viewer -> 200
|
|
||||||
self.client.login(username="viewer", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertContains(res, "ZG-TEST-01")
|
|
||||||
self.assertFalse(res.context['can_create'])
|
|
||||||
|
|
||||||
# Editor -> 200 and can_create is True
|
|
||||||
self.client.login(username="editor", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertTrue(res.context['can_create'])
|
|
||||||
|
|
||||||
def test_management_zone_create_view(self):
|
|
||||||
url = reverse("assets:management_zone_create")
|
|
||||||
|
|
||||||
# Viewer cannot create -> 403
|
|
||||||
self.client.login(username="viewer", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 403)
|
|
||||||
|
|
||||||
# Editor can access form
|
|
||||||
self.client.login(username="editor", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertContains(res, "layer-switcher-control.js")
|
|
||||||
self.assertContains(res, "AddressSearchControl")
|
|
||||||
|
|
||||||
# Post valid zone
|
|
||||||
geojson_polygon = {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [[[4.35, 50.84], [4.36, 50.84], [4.36, 50.85], [4.35, 50.85], [4.35, 50.84]]]
|
|
||||||
}
|
|
||||||
post_data = {
|
|
||||||
'code': 'ZG-CREATED-01',
|
|
||||||
'name_fr': 'Zone créée',
|
|
||||||
'thematic': self.thematic_nature.pk,
|
|
||||||
'zone_type': self.zone_type.pk,
|
|
||||||
'color': '#28a745',
|
|
||||||
'target_frequency_days': 15,
|
|
||||||
'geom_geojson': json.dumps(geojson_polygon),
|
|
||||||
'instructions': 'Passage le matin uniquement',
|
|
||||||
'is_active': True,
|
|
||||||
}
|
|
||||||
res = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
|
|
||||||
created_zone = ManagementZone.objects.get(code='ZG-CREATED-01')
|
|
||||||
self.assertEqual(created_zone.created_by, self.user_editor)
|
|
||||||
self.assertEqual(created_zone.instructions, 'Passage le matin uniquement')
|
|
||||||
self.assertEqual(created_zone.geom.srid, 3812)
|
|
||||||
|
|
||||||
def test_management_zone_edit_view(self):
|
|
||||||
url = reverse("assets:management_zone_edit", kwargs={'pk': self.zone.pk})
|
|
||||||
|
|
||||||
# Viewer cannot edit -> 403
|
|
||||||
self.client.login(username="viewer", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 403)
|
|
||||||
|
|
||||||
# Editor can edit
|
|
||||||
self.client.login(username="editor", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertContains(res, "layer-switcher-control.js")
|
|
||||||
self.assertContains(res, "AddressSearchControl")
|
|
||||||
|
|
||||||
post_data = {
|
|
||||||
'code': self.zone.code,
|
|
||||||
'name_fr': 'Nom Modifié Marais',
|
|
||||||
'thematic': self.thematic_nature.pk,
|
|
||||||
'zone_type': self.zone_type.pk,
|
|
||||||
'color': '#ff5500',
|
|
||||||
'target_frequency_days': 120,
|
|
||||||
'geom_geojson': json.dumps({
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [[[4.35, 50.84], [4.37, 50.84], [4.37, 50.86], [4.35, 50.86], [4.35, 50.84]]]
|
|
||||||
}),
|
|
||||||
'instructions': 'Consigne mise à jour',
|
|
||||||
'is_active': True,
|
|
||||||
}
|
|
||||||
res = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
|
|
||||||
self.zone.refresh_from_db()
|
|
||||||
self.assertEqual(self.zone.name_fr, 'Nom Modifié Marais')
|
|
||||||
self.assertEqual(self.zone.color, '#ff5500')
|
|
||||||
self.assertEqual(self.zone.target_frequency_days, 120)
|
|
||||||
|
|
||||||
def test_management_zone_detail_view(self):
|
|
||||||
url = reverse("assets:management_zone_detail", kwargs={'pk': self.zone.pk})
|
|
||||||
|
|
||||||
# Viewer can view
|
|
||||||
self.client.login(username="viewer", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertContains(res, "ZG-TEST-01")
|
|
||||||
self.assertFalse(res.context['can_edit'])
|
|
||||||
|
|
||||||
# Editor can view with can_edit=True
|
|
||||||
self.client.login(username="editor", password="password")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertTrue(res.context['can_edit'])
|
|
||||||
|
|
||||||
def test_management_zone_delete_view(self):
|
|
||||||
zone_to_del = ManagementZone.objects.create(
|
|
||||||
code="ZG-TO-DELETE",
|
|
||||||
name_fr="A supprimer",
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
)
|
|
||||||
url = reverse("assets:management_zone_delete", kwargs={'pk': zone_to_del.pk})
|
|
||||||
|
|
||||||
# Viewer cannot delete -> 403
|
|
||||||
self.client.login(username="viewer", password="password")
|
|
||||||
res = self.client.post(url)
|
|
||||||
self.assertEqual(res.status_code, 403)
|
|
||||||
|
|
||||||
# Editor can delete
|
|
||||||
self.client.login(username="editor", password="password")
|
|
||||||
res = self.client.post(url)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
self.assertFalse(ManagementZone.objects.filter(pk=zone_to_del.pk).exists())
|
|
||||||
|
|
||||||
def test_management_zone_document_upload_and_delete(self):
|
|
||||||
self.client.login(username="editor", password="password")
|
|
||||||
|
|
||||||
# Upload document
|
|
||||||
upload_url = reverse("assets:management_zone_add_document", kwargs={'pk': self.zone.pk})
|
|
||||||
fake_file = SimpleUploadedFile("rapport_tonte.pdf", b"Dummy PDF content", content_type="application/pdf")
|
|
||||||
res = self.client.post(upload_url, {'documents': [fake_file]})
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
|
|
||||||
self.assertEqual(self.zone.documents.count(), 1)
|
|
||||||
doc = self.zone.documents.first()
|
|
||||||
self.assertEqual(doc.uploaded_by, self.user_editor)
|
|
||||||
|
|
||||||
# Delete document
|
|
||||||
delete_url = reverse("assets:management_zone_delete_document", kwargs={'pk': self.zone.pk, 'document_id': doc.pk})
|
|
||||||
res = self.client.post(delete_url)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
self.assertEqual(self.zone.documents.count(), 0)
|
|
||||||
|
|
||||||
def test_management_zone_types_ajax(self):
|
|
||||||
self.client.login(username="editor", password="password")
|
|
||||||
url = reverse("assets:management_zone_types_ajax") + f"?thematic_id={self.thematic_nature.pk}"
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
data = res.json()
|
|
||||||
self.assertEqual(len(data['types']), 1)
|
|
||||||
self.assertEqual(data['types'][0]['code'], 'fauche_eco')
|
|
||||||
|
|
||||||
|
|
||||||
class ManagementZoneUnifiedViewIntegrationTests(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.thematic_nature = Thematic.objects.create(name_fr="Nature", name_nl="Natuur", code="nature")
|
|
||||||
self.thematic_roads = Thematic.objects.create(name_fr="Voirie", name_nl="Wegen", code="roads")
|
|
||||||
|
|
||||||
self.user_editor = User.objects.create_user(username="editor_unified", password="password")
|
|
||||||
self.user_viewer = User.objects.create_user(username="viewer_unified", password="password")
|
|
||||||
|
|
||||||
self.config_editor = UserConfig.objects.create(user=self.user_editor)
|
|
||||||
self.config_viewer = UserConfig.objects.create(user=self.user_viewer)
|
|
||||||
|
|
||||||
UserThematics.objects.create(
|
|
||||||
user_config=self.config_editor,
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
can_view_assets=True,
|
|
||||||
can_edit_assets=True,
|
|
||||||
can_edit_management_zones=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
UserThematics.objects.create(
|
|
||||||
user_config=self.config_viewer,
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
can_view_assets=True,
|
|
||||||
can_edit_assets=False,
|
|
||||||
can_edit_management_zones=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.zone_type = ManagementZoneType.objects.create(
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
code="fauche_eco",
|
|
||||||
name_fr="Fauche tardive",
|
|
||||||
name_nl="Laat maaien",
|
|
||||||
default_color="#27ae60",
|
|
||||||
)
|
|
||||||
|
|
||||||
poly = Polygon(((149000, 170000), (149100, 170000), (149100, 170100), (149000, 170100), (149000, 170000)))
|
|
||||||
multi_poly = MultiPolygon([poly], srid=3812)
|
|
||||||
self.zone = ManagementZone.objects.create(
|
|
||||||
thematic=self.thematic_nature,
|
|
||||||
zone_type=self.zone_type,
|
|
||||||
code="ZN-TEST-001",
|
|
||||||
name_fr="Pelouse A",
|
|
||||||
name_nl="Grasveld A",
|
|
||||||
geom=multi_poly,
|
|
||||||
is_active=True,
|
|
||||||
created_by=self.user_editor,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_build_thematic_config_includes_management_zones_url(self):
|
|
||||||
from assets.views.core import build_thematic_config
|
|
||||||
config = build_thematic_config([self.thematic_nature, self.thematic_roads])
|
|
||||||
self.assertIn("nature", config)
|
|
||||||
self.assertIn("management_zones_geojson_url", config["nature"])
|
|
||||||
self.assertEqual(config["nature"]["management_zones_geojson_url"], reverse("assets:management_zones_geojson"))
|
|
||||||
|
|
||||||
def test_build_unified_add_modal_config_permissions(self):
|
|
||||||
from assets.views.core import build_unified_add_modal_config
|
|
||||||
# Editor can edit management zones in nature
|
|
||||||
editor_config = build_unified_add_modal_config([self.thematic_nature], user=self.user_editor)
|
|
||||||
self.assertIn("management_zone_thematics", editor_config)
|
|
||||||
editor_codes = [t["code"] for t in editor_config["management_zone_thematics"]]
|
|
||||||
self.assertIn("nature", editor_codes)
|
|
||||||
|
|
||||||
# Viewer cannot edit management zones
|
|
||||||
viewer_config = build_unified_add_modal_config([self.thematic_nature], user=self.user_viewer)
|
|
||||||
viewer_codes = [t["code"] for t in viewer_config["management_zone_thematics"]]
|
|
||||||
self.assertNotIn("nature", viewer_codes)
|
|
||||||
|
|
||||||
def test_management_zones_geojson_properties_for_unified_table(self):
|
|
||||||
self.client.login(username="editor_unified", password="password")
|
|
||||||
url = reverse("assets:management_zones_geojson") + f"?thematic={self.thematic_nature.code}"
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
data = res.json()
|
|
||||||
self.assertEqual(len(data["features"]), 1)
|
|
||||||
feat = data["features"][0]
|
|
||||||
props = feat["properties"]
|
|
||||||
|
|
||||||
self.assertTrue(props["is_management_zone"])
|
|
||||||
self.assertFalse(props["is_location"])
|
|
||||||
self.assertEqual(props["asset_type"], "managementzone")
|
|
||||||
self.assertIn(props["element_type"], ["Zone de gestion", "Management zone"])
|
|
||||||
self.assertEqual(props["category"], "Fauche tardive")
|
|
||||||
self.assertEqual(props["status"], "active")
|
|
||||||
self.assertIsNotNone(props["lon"])
|
|
||||||
self.assertIsNotNone(props["lat"])
|
|
||||||
self.assertIsNotNone(props["area"])
|
|
||||||
self.assertEqual(props["detail_url"], reverse("assets:management_zone_detail", kwargs={"pk": self.zone.pk}))
|
|
||||||
|
|
||||||
def test_unified_index_page_contains_management_zone_elements(self):
|
|
||||||
self.client.login(username="editor_unified", password="password")
|
|
||||||
url = reverse("assets:index")
|
|
||||||
res = self.client.get(url)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
|
|
||||||
# Check filter modal checkboxes
|
|
||||||
self.assertContains(res, 'id="type-cb-assets"')
|
|
||||||
self.assertContains(res, 'id="type-cb-locations"')
|
|
||||||
self.assertContains(res, 'id="type-cb-management-zones"')
|
|
||||||
|
|
||||||
# Check add modal card
|
|
||||||
self.assertContains(res, 'id="unified-add-management-zone-thematic"')
|
|
||||||
self.assertContains(res, 'id="unified-add-management-zone-open"')
|
|
||||||
|
|
@ -1,19 +1,9 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import core, locations, nature, publiclighting, trafficlights, artworks, structures, controlcenters, sign, roads, its, clean, asset_editing, history, location_editing, export, asset_permissions, water, buildings, outdoor, parking, management_zones
|
from .views import core, locations, nature, publiclighting, trafficlights, artworks, structures, controlcenters, sign, roads, its, clean, asset_editing, history, location_editing, export, asset_permissions, water, buildings, outdoor, parking
|
||||||
|
|
||||||
app_name = "assets"
|
app_name = "assets"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", core.unified_assets_index, name="index"),
|
path("", core.unified_assets_index, name="index"),
|
||||||
# Management Zones (Zones de gestion)
|
|
||||||
path("management-zones/", management_zones.management_zones_index, name="management_zones_index"),
|
|
||||||
path("management-zones/add/", management_zones.management_zone_create, name="management_zone_create"),
|
|
||||||
path("management-zones/types/ajax/", management_zones.management_zone_types_ajax, name="management_zone_types_ajax"),
|
|
||||||
path("management-zones/<int:pk>/", management_zones.management_zone_detail, name="management_zone_detail"),
|
|
||||||
path("management-zones/<int:pk>/edit/", management_zones.management_zone_edit, name="management_zone_edit"),
|
|
||||||
path("management-zones/<int:pk>/delete/", management_zones.management_zone_delete, name="management_zone_delete"),
|
|
||||||
path("management-zones/<int:pk>/add-document/", management_zones.management_zone_add_document, name="management_zone_add_document"),
|
|
||||||
path("management-zones/<int:pk>/documents/<int:document_id>/delete/", management_zones.management_zone_delete_document, name="management_zone_delete_document"),
|
|
||||||
path("api/management-zones/geojson/", management_zones.management_zones_geojson, name="management_zones_geojson"),
|
|
||||||
path("parking/", parking.parking_index, name="parking_index"),
|
path("parking/", parking.parking_index, name="parking_index"),
|
||||||
path("parking/assets/", parking.parking_assets, name="parking_assets"),
|
path("parking/assets/", parking.parking_assets, name="parking_assets"),
|
||||||
path("parking/assets/geojson/", parking.parking_assets_geojson, name="parking_assets_geojson"),
|
path("parking/assets/geojson/", parking.parking_assets_geojson, name="parking_assets_geojson"),
|
||||||
|
|
@ -24,7 +14,6 @@ urlpatterns = [
|
||||||
path("buildings/", buildings.buildings_index, name="buildings_index"),
|
path("buildings/", buildings.buildings_index, name="buildings_index"),
|
||||||
path("buildings/<int:pk>/", buildings.building_detail, name="publicbuilding_detail"),
|
path("buildings/<int:pk>/", buildings.building_detail, name="publicbuilding_detail"),
|
||||||
path("buildings/<int:pk>/edit/", buildings.publicbuilding_edit, name="publicbuilding_edit"),
|
path("buildings/<int:pk>/edit/", buildings.publicbuilding_edit, name="publicbuilding_edit"),
|
||||||
path("buildings/<int:pk>/floor-plan/", buildings.publicbuilding_plan_file, name="publicbuilding_plan_file"),
|
|
||||||
path("buildings/<str:asset_model>/<int:asset_id>/", buildings.publicbuildings_assets_detail, name="publicbuildings_assets_detail"),
|
path("buildings/<str:asset_model>/<int:asset_id>/", buildings.publicbuildings_assets_detail, name="publicbuildings_assets_detail"),
|
||||||
path("export/", export.export_assets, name="export_assets"),
|
path("export/", export.export_assets, name="export_assets"),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,56 +26,6 @@ def is_hp_or_ls(text):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_wiring_or_non_equipment(text):
|
|
||||||
"""
|
|
||||||
Checks if the wire/direction/circuit text refers to auxiliary wiring, communication,
|
|
||||||
power supply, contact lines, or reserve wires that must NOT be treated as equipment
|
|
||||||
(neither lantern nor detector).
|
|
||||||
In cross plans, 'Com' / 'Comm' / 'Communication' represents communication wiring/bus
|
|
||||||
for detectors/cameras or radar power supplies, NOT an actual detector.
|
|
||||||
"""
|
|
||||||
if not text:
|
|
||||||
return False
|
|
||||||
t_str = str(text).strip()
|
|
||||||
t_lower = t_str.lower()
|
|
||||||
t_clean = re.sub(r'[\s/\\_\-\.:;]+', ' ', t_lower).strip()
|
|
||||||
t_upper = re.sub(r'\s+', '', t_str.upper())
|
|
||||||
|
|
||||||
# 1. Direct keyword matches (word boundary)
|
|
||||||
if re.search(r'\b(?:com|comm|communication|communicatie|alim|alimentation|voeding|tension|contact|reserve|reservee|réservé|réservée|réserve|libre|vrij|spaar|terre|masse|neutre)\b', t_clean, re.IGNORECASE):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 2. String starts with or equals COM, COMM, ALIM, etc.
|
|
||||||
if re.match(r'^(?:COM|COMM|ALIM|CONTACT|TENSION|RESERVE)\d*$', t_upper, re.IGNORECASE):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if t_upper.startswith(("COM_", "COMM_", "ALIM_", "COM-", "COMM-", "ALIM-")):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 3. Radar power supply (not a detector code like RA, RB, RC, RD, RM, RP)
|
|
||||||
if t_upper == "RADAR" or t_upper.startswith("RADAR") or (bool(re.search(r'\bradar\b', t_str, re.IGNORECASE)) and not bool(re.match(r'^(?:RA|RB|RC|RD|RM|RP)\d*$', t_upper, re.IGNORECASE))):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_cyclopieton_phase(phase):
|
|
||||||
"""
|
|
||||||
Checks if a phase code corresponds to a combined pedestrian + cyclist signal (cyclo-piéton).
|
|
||||||
Standard notation: lowercase letter (pedestrian) + 'F' or 'f' (fiets/cyclist), e.g. 'bF', 'aF', 'cF', 'dF', 'bF1',
|
|
||||||
or vice versa ('Fa', 'Fb').
|
|
||||||
"""
|
|
||||||
if not phase:
|
|
||||||
return False
|
|
||||||
ph = str(phase).strip()
|
|
||||||
if re.match(r'^[a-z][Ff]\d*$', ph) or re.match(r'^[Ff][a-z]\d*$', ph):
|
|
||||||
return True
|
|
||||||
if re.match(r'^[a-z]\s*[\+/&]\s*[Ff]\d*$', ph) or re.match(r'^[Ff]\s*[\+/&]\s*[a-z]\d*$', ph):
|
|
||||||
return True
|
|
||||||
ph_lower = ph.lower()
|
|
||||||
if 'cyclo' in ph_lower and ('piet' in ph_lower or 'piéto' in ph_lower or 'ped' in ph_lower):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def parse_cross_plan(excel_path):
|
def parse_cross_plan(excel_path):
|
||||||
"""
|
"""
|
||||||
Parses a crossroads cross plan (kruisjesplan) Excel file and returns
|
Parses a crossroads cross plan (kruisjesplan) Excel file and returns
|
||||||
|
|
@ -315,11 +265,6 @@ def parse_cross_plan(excel_path):
|
||||||
pole_has_hp[p] = True
|
pole_has_hp[p] = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if wire refers to communication, power supply, contact, reserve, etc.
|
|
||||||
# It must NOT be treated as an equipment (neither lantern nor detector).
|
|
||||||
if is_wiring_or_non_equipment(richting) or is_wiring_or_non_equipment(kringen):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Detectors
|
# Detectors
|
||||||
# Check if push button (e.g. Dka, Dkb, PBa1, BPf4...)
|
# Check if push button (e.g. Dka, Dkb, PBa1, BPf4...)
|
||||||
if richting.lower().startswith(("dk", "pb", "bp")):
|
if richting.lower().startswith(("dk", "pb", "bp")):
|
||||||
|
|
@ -420,7 +365,7 @@ def parse_cross_plan(excel_path):
|
||||||
# Richting codes are typically: A, B, C, T1, T2, F1, F2, a, b, c...
|
# Richting codes are typically: A, B, C, T1, T2, F1, F2, a, b, c...
|
||||||
# Ignore wiring/cabling categories like ALIM, Radar, Com, contact, HP/LS, etc.
|
# Ignore wiring/cabling categories like ALIM, Radar, Com, contact, HP/LS, etc.
|
||||||
normalized = re.sub(r'\s+', ' ', richting).strip()
|
normalized = re.sub(r'\s+', ' ', richting).strip()
|
||||||
if is_hp_or_ls(richting) or is_hp_or_ls(kringen) or is_wiring_or_non_equipment(richting) or is_wiring_or_non_equipment(kringen) or re.search(r'(?:alim|com|contact|radar|tension|comm|\bhp\b|\bls\b|hp/ls|ls/hp)', normalized, re.IGNORECASE):
|
if is_hp_or_ls(richting) or is_hp_or_ls(kringen) or re.search(r'(?:alim|com|contact|radar|tension|comm|\bhp\b|\bls\b|hp/ls|ls/hp)', normalized, re.IGNORECASE):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if p not in pole_phases:
|
if p not in pole_phases:
|
||||||
|
|
@ -430,13 +375,9 @@ def parse_cross_plan(excel_path):
|
||||||
# Now generate LANTERNS for each pole
|
# Now generate LANTERNS for each pole
|
||||||
deduced_lanterns = []
|
deduced_lanterns = []
|
||||||
|
|
||||||
# helper to sort phases: vehicles first, then trams, then cycles, then cyclo-piétons / pedestrians
|
# helper to sort phases: vehicles first, then trams, then cycles, then pedestrians
|
||||||
def phase_sort_key(ph):
|
def phase_sort_key(ph):
|
||||||
if is_cyclopieton_phase(ph):
|
if ph and ph[0].islower(): # Pedestrian
|
||||||
if ph and ph[0].islower():
|
|
||||||
return (3, ph)
|
|
||||||
return (2, ph)
|
|
||||||
elif ph and ph[0].islower(): # Pedestrian
|
|
||||||
return (3, ph)
|
return (3, ph)
|
||||||
elif ph.startswith("T"): # Tram
|
elif ph.startswith("T"): # Tram
|
||||||
return (1, ph)
|
return (1, ph)
|
||||||
|
|
@ -463,10 +404,7 @@ def parse_cross_plan(excel_path):
|
||||||
|
|
||||||
if is_mast_arm and has_double_marker:
|
if is_mast_arm and has_double_marker:
|
||||||
# Determine models for both lanterns
|
# Determine models for both lanterns
|
||||||
if is_cyclopieton_phase(phase):
|
if phase and phase[0].islower(): # Pedestrian
|
||||||
model_name_1 = "2V200 cyclo-piéto + HP" if has_hp else "2V200 cyclo-piéto"
|
|
||||||
model_name_2 = "2V200 cyclo-piéto + HP" if has_hp else "2V200 cyclo-piéto"
|
|
||||||
elif phase and phase[0].islower(): # Pedestrian
|
|
||||||
model_name_1 = "2V200 piéton+HP" if has_hp else "2V200"
|
model_name_1 = "2V200 piéton+HP" if has_hp else "2V200"
|
||||||
model_name_2 = "2V200 piéton+HP" if has_hp else "2V200"
|
model_name_2 = "2V200 piéton+HP" if has_hp else "2V200"
|
||||||
elif phase.startswith("T"): # Tram
|
elif phase.startswith("T"): # Tram
|
||||||
|
|
@ -514,12 +452,7 @@ def parse_cross_plan(excel_path):
|
||||||
lantern_idx += 1
|
lantern_idx += 1
|
||||||
else:
|
else:
|
||||||
# Deduce single model
|
# Deduce single model
|
||||||
if is_cyclopieton_phase(phase):
|
if phase and phase[0].islower(): # Pedestrian
|
||||||
if has_hp:
|
|
||||||
model_name = "2V200 cyclo-piéto + HP"
|
|
||||||
else:
|
|
||||||
model_name = "2V200 cyclo-piéto"
|
|
||||||
elif phase and phase[0].islower(): # Pedestrian
|
|
||||||
if has_hp:
|
if has_hp:
|
||||||
model_name = "2V200 piéton+HP"
|
model_name = "2V200 piéton+HP"
|
||||||
else:
|
else:
|
||||||
|
|
@ -609,23 +542,9 @@ def classify_lantern(model_name="", phase="", code="", name=""):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check Cyclo-piéton:
|
|
||||||
is_cyclopieton = (
|
|
||||||
not is_tram and not is_bus and (
|
|
||||||
is_cyclopieton_phase(phase_clean)
|
|
||||||
or bool(re.search(r'[\b_]LAN\d+_[a-z][Ff]\d*', code_upper))
|
|
||||||
or "cyclo-piét" in combined_text
|
|
||||||
or "cyclo-piet" in combined_text
|
|
||||||
or "cyclopiet" in combined_text
|
|
||||||
or ("cyclo" in combined_text and any(w in combined_text for w in ["piét", "piet", "voetganger"]))
|
|
||||||
or (any(w in combined_text for w in ["cycliste", "velo", "vélo", "fiets"])
|
|
||||||
and any(w in combined_text for w in ["piéton", "pieton", "voetganger", "pedestrian"]))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check Cyclist:
|
# Check Cyclist:
|
||||||
is_cyclist = (
|
is_cyclist = (
|
||||||
not is_tram and not is_bus and not is_cyclopieton and (
|
not is_tram and not is_bus and (
|
||||||
any(w in combined_text for w in ["cycliste", "cycl", "velo", "vélo", "fiets"])
|
any(w in combined_text for w in ["cycliste", "cycl", "velo", "vélo", "fiets"])
|
||||||
or phase_clean.upper().startswith("F")
|
or phase_clean.upper().startswith("F")
|
||||||
or bool(re.search(r'\bF\d+\b', code_upper))
|
or bool(re.search(r'\bF\d+\b', code_upper))
|
||||||
|
|
@ -634,7 +553,7 @@ def classify_lantern(model_name="", phase="", code="", name=""):
|
||||||
|
|
||||||
# Check Pedestrian:
|
# Check Pedestrian:
|
||||||
is_pedestrian = (
|
is_pedestrian = (
|
||||||
not is_tram and not is_bus and not is_cyclopieton and not is_cyclist and (
|
not is_tram and not is_bus and not is_cyclist and (
|
||||||
any(w in combined_text for w in ["piéton", "pieton", "voetganger", "pedestrian"])
|
any(w in combined_text for w in ["piéton", "pieton", "voetganger", "pedestrian"])
|
||||||
or (bool(phase_clean) and phase_clean[0].islower() and not phase_clean.startswith("fl"))
|
or (bool(phase_clean) and phase_clean[0].islower() and not phase_clean.startswith("fl"))
|
||||||
or phase_clean.upper().startswith("P")
|
or phase_clean.upper().startswith("P")
|
||||||
|
|
@ -646,8 +565,6 @@ def classify_lantern(model_name="", phase="", code="", name=""):
|
||||||
category = "tram"
|
category = "tram"
|
||||||
elif is_bus:
|
elif is_bus:
|
||||||
category = "bus"
|
category = "bus"
|
||||||
elif is_cyclopieton:
|
|
||||||
category = "cyclopieton"
|
|
||||||
elif is_cyclist:
|
elif is_cyclist:
|
||||||
category = "cyclist"
|
category = "cyclist"
|
||||||
elif is_pedestrian:
|
elif is_pedestrian:
|
||||||
|
|
@ -717,13 +634,7 @@ def calculate_lantern_match_score(dl, el, is_single_on_pole=False):
|
||||||
return 10000
|
return 10000
|
||||||
|
|
||||||
dl_base_code = dl_code.rsplit("_", 1)[0] if "_" in dl_code else dl_code
|
dl_base_code = dl_code.rsplit("_", 1)[0] if "_" in dl_code else dl_code
|
||||||
el_base_code = el_code.rsplit("_", 1)[0] if "_" in el_code else el_code
|
is_prefix_code_match = bool(dl_base_code and el_code and dl_base_code == el_code)
|
||||||
|
|
||||||
is_prefix_code_match = bool(
|
|
||||||
(dl_base_code and el_code and dl_base_code == el_code)
|
|
||||||
or (dl_code and el_base_code and dl_code == el_base_code)
|
|
||||||
or (dl_base_code and el_base_code and dl_base_code == el_base_code)
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_single_on_pole and is_prefix_code_match:
|
if is_single_on_pole and is_prefix_code_match:
|
||||||
return 1000
|
return 1000
|
||||||
|
|
@ -753,12 +664,10 @@ def calculate_lantern_match_score(dl, el, is_single_on_pole=False):
|
||||||
|
|
||||||
# 2. Category matching
|
# 2. Category matching
|
||||||
if dl_cat == el_cat:
|
if dl_cat == el_cat:
|
||||||
if dl_cat in ("tram", "bus", "pedestrian", "cyclist", "cyclopieton"):
|
if dl_cat in ("tram", "bus", "pedestrian", "cyclist"):
|
||||||
score += 300
|
score += 300
|
||||||
else:
|
else:
|
||||||
score += 200
|
score += 200
|
||||||
elif {dl_cat, el_cat} in ({"cyclopieton", "pedestrian"}, {"cyclopieton", "cyclist"}):
|
|
||||||
score += 50
|
|
||||||
else:
|
else:
|
||||||
# Category mismatch penalty: e.g. tram vs car, pedestrian vs car
|
# Category mismatch penalty: e.g. tram vs car, pedestrian vs car
|
||||||
score -= 600
|
score -= 600
|
||||||
|
|
@ -798,9 +707,9 @@ def calculate_lantern_match_score(dl, el, is_single_on_pole=False):
|
||||||
else:
|
else:
|
||||||
score -= 60
|
score -= 60
|
||||||
|
|
||||||
# 8. Prefix match bonus (e.g. LAN03 vs LAN03_bF on the same pole)
|
# 8. Prefix match bonus
|
||||||
if is_prefix_code_match:
|
if is_prefix_code_match:
|
||||||
score += 250
|
score += 30
|
||||||
|
|
||||||
# 9. LAN index tie-breaker (e.g. LAN01 vs LAN01)
|
# 9. LAN index tie-breaker (e.g. LAN01 vs LAN01)
|
||||||
m_dl = re.search(r'LAN(\d+)', dl_code, re.IGNORECASE)
|
m_dl = re.search(r'LAN(\d+)', dl_code, re.IGNORECASE)
|
||||||
|
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
import math
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
# Coefficients allométriques carbone par genre botanique (t CO2 / (cm_circ * m_hauteur))
|
|
||||||
# Dérivés des tables de biomasse et densités ligneuses (i-Tree / Chave / Grality)
|
|
||||||
GENUS_CARBON_K = {
|
|
||||||
"aesculus": 0.02471, # Marronnier
|
|
||||||
"carpinus": 0.02370, # Charme
|
|
||||||
"quercus": 0.01952, # Chêne
|
|
||||||
"betula": 0.01952, # Bouleau
|
|
||||||
"fagus": 0.01882, # Hêtre
|
|
||||||
"prunus": 0.01813, # Cerisier / Prunier
|
|
||||||
"fraxinus": 0.01743, # Frêne
|
|
||||||
"acer": 0.01673, # Érable
|
|
||||||
"platanus": 0.01673, # Platane
|
|
||||||
"castanea": 0.01900, # Châtaignier
|
|
||||||
"robinia": 0.01850, # Robinier
|
|
||||||
"ulmus": 0.01750, # Orme
|
|
||||||
"cedrus": 0.01600, # Cèdre
|
|
||||||
"salix": 0.01550, # Saule
|
|
||||||
"populus": 0.01500, # Peuplier
|
|
||||||
"pinus": 0.01450, # Pin
|
|
||||||
"tilia": 0.01366, # Tilleul
|
|
||||||
"malus": 0.01800, # Pommier
|
|
||||||
"pyrus": 0.01800, # Poirier
|
|
||||||
}
|
|
||||||
|
|
||||||
# Indices écologiques de biodiversité par genre / essence
|
|
||||||
GENUS_BIODIVERSITY = {
|
|
||||||
"quercus": 0.92, # Chêne (très forte valeur d'accueil)
|
|
||||||
"fagus": 0.88, # Hêtre
|
|
||||||
"platanus": 0.85, # Platane
|
|
||||||
"fraxinus": 0.78, # Frêne
|
|
||||||
"tilia": 0.78, # Tilleul
|
|
||||||
"carpinus": 0.76, # Charme
|
|
||||||
"salix": 0.75, # Saule
|
|
||||||
"acer": 0.68, # Érable
|
|
||||||
"aesculus": 0.62, # Marronnier
|
|
||||||
"pinus": 0.62, # Pin
|
|
||||||
"populus": 0.60, # Peuplier
|
|
||||||
"ulmus": 0.60, # Orme
|
|
||||||
"malus": 0.53, # Pommier
|
|
||||||
"betula": 0.47, # Bouleau
|
|
||||||
"prunus": 0.44, # Cerisier / Prunus ornemental
|
|
||||||
}
|
|
||||||
|
|
||||||
# Constante d'énergie de refroidissement par an (kWh / m^2 de canopée projetée)
|
|
||||||
# Dérivée de l'évapotranspiration urbaine Grality : 296.357 * D^2
|
|
||||||
COOLING_ENERGY_PER_D2 = 296.357
|
|
||||||
|
|
||||||
# Valeur de base au cm² de section de tronc (Barème de l'Arbre / Plante & Cité)
|
|
||||||
BASE_AMENITY_VALUE_PER_CM2 = 3.20
|
|
||||||
|
|
||||||
|
|
||||||
def get_genus_carbon_k(botanical_str: Optional[str]) -> float:
|
|
||||||
"""Retourne le coefficient allométrique de carbone pour l'essence ou genre spécifié."""
|
|
||||||
if not botanical_str:
|
|
||||||
return 0.0180
|
|
||||||
b_lower = botanical_str.lower()
|
|
||||||
for genus, k in GENUS_CARBON_K.items():
|
|
||||||
if genus in b_lower:
|
|
||||||
return k
|
|
||||||
return 0.0180
|
|
||||||
|
|
||||||
|
|
||||||
def get_genus_biodiversity_index(botanical_str: Optional[str]) -> float:
|
|
||||||
"""Retourne l'indice de biodiversité pour l'essence ou genre spécifié."""
|
|
||||||
if not botanical_str:
|
|
||||||
return 0.60
|
|
||||||
b_lower = botanical_str.lower()
|
|
||||||
for genus, b in GENUS_BIODIVERSITY.items():
|
|
||||||
if genus in b_lower:
|
|
||||||
return b
|
|
||||||
return 0.60
|
|
||||||
|
|
||||||
|
|
||||||
def compute_tree_ecoservices(tree, overwrite_existing: bool = False) -> bool:
|
|
||||||
"""
|
|
||||||
Calcule et met à jour automatiquement les indicateurs écosystémiques et patrimoniaux
|
|
||||||
d'un arbre (NatureTree) :
|
|
||||||
1. Dimensions déduites si manquantes (circonférence et diamètre de houppier).
|
|
||||||
2. Valeur d'aménité (€) selon le Barème de l'Arbre (section du tronc et coefficients).
|
|
||||||
3. Stock de carbone (t CO2) selon l'allométrie dendrométrique et l'essence.
|
|
||||||
4. Énergie de refroidissement (kWh) et Indice de fraîcheur selon la canopée.
|
|
||||||
5. Indice de biodiversité selon l'essence.
|
|
||||||
|
|
||||||
Si overwrite_existing=False (par défaut), ne remplace pas les valeurs déjà renseignées
|
|
||||||
(ex: valeurs officielles Grality déjà enregistrées).
|
|
||||||
Retourne True si au moins un champ a été calculé/mis à jour.
|
|
||||||
"""
|
|
||||||
changed = False
|
|
||||||
|
|
||||||
# 1. Estimation / déduction des dimensions manquantes à partir de la hauteur ou circonférence
|
|
||||||
h = getattr(tree, 'height', None)
|
|
||||||
c = getattr(tree, 'circumference', None)
|
|
||||||
crown_d = getattr(tree, 'crown_diameter', None)
|
|
||||||
|
|
||||||
if (not c or c <= 0) and h and h > 0:
|
|
||||||
c = round(h * 7.5, 1)
|
|
||||||
tree.circumference = c
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
if (not crown_d or crown_d <= 0):
|
|
||||||
if h and h > 0:
|
|
||||||
crown_d = round(max(2.0, min(18.0, h * 0.45)), 1)
|
|
||||||
tree.crown_diameter = crown_d
|
|
||||||
changed = True
|
|
||||||
elif c and c > 0:
|
|
||||||
estimated_h = c / 7.5
|
|
||||||
crown_d = round(max(2.0, min(18.0, estimated_h * 0.45)), 1)
|
|
||||||
tree.crown_diameter = crown_d
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
if (not h or h <= 0) and c and c > 0:
|
|
||||||
h = round(c / 7.5, 1)
|
|
||||||
tree.height = h
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# 2. Coefficients de situation et plantation par défaut si absents
|
|
||||||
sit = getattr(tree, 'situation_coefficient', None)
|
|
||||||
if sit is None or overwrite_existing:
|
|
||||||
ctx = getattr(tree, 'implantation_context', 'street')
|
|
||||||
sit = 5.0 if ctx in ('park', 'square', 'isolated') else 4.0
|
|
||||||
tree.situation_coefficient = sit
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
plant = getattr(tree, 'plantation_coefficient', None)
|
|
||||||
if plant is None or overwrite_existing:
|
|
||||||
plant = 5.0
|
|
||||||
tree.plantation_coefficient = plant
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# Statut sanitaire / vitalité
|
|
||||||
is_dead = bool(getattr(tree, 'is_dead', False))
|
|
||||||
vitality = getattr(tree, 'vitality', 'normal') or 'normal'
|
|
||||||
|
|
||||||
if is_dead or vitality == 'dead':
|
|
||||||
sa_coeff = 0.0
|
|
||||||
elif vitality == 'declining':
|
|
||||||
sa_coeff = 0.3
|
|
||||||
elif vitality == 'weak':
|
|
||||||
sa_coeff = 0.6
|
|
||||||
else:
|
|
||||||
sa_coeff = 1.0
|
|
||||||
|
|
||||||
# 3. Valeur d'aménité (€) — Barème de l'Arbre
|
|
||||||
if getattr(tree, 'amenity_value', None) is None or overwrite_existing:
|
|
||||||
if sa_coeff == 0.0:
|
|
||||||
tree.amenity_value = 0.0
|
|
||||||
changed = True
|
|
||||||
elif c and c > 0:
|
|
||||||
section_cm2 = (c ** 2) / (4.0 * math.pi)
|
|
||||||
tree.amenity_value = round(
|
|
||||||
section_cm2 * BASE_AMENITY_VALUE_PER_CM2 * (sit / 5.0) * (plant / 5.0) * sa_coeff,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# 4. Énergie de refroidissement (kWh) et Indice de fraîcheur
|
|
||||||
if getattr(tree, 'cooling_energy_indicator', None) is None or overwrite_existing:
|
|
||||||
if is_dead or vitality == 'dead':
|
|
||||||
tree.cooling_energy_indicator = 0.0
|
|
||||||
changed = True
|
|
||||||
elif crown_d and crown_d > 0:
|
|
||||||
tree.cooling_energy_indicator = round(COOLING_ENERGY_PER_D2 * (crown_d ** 2), 2)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
if getattr(tree, 'cooling_indicator', None) is None or overwrite_existing:
|
|
||||||
if is_dead or vitality == 'dead':
|
|
||||||
tree.cooling_indicator = 0.0
|
|
||||||
changed = True
|
|
||||||
elif tree.cooling_energy_indicator is not None and tree.cooling_energy_indicator > 0:
|
|
||||||
tree.cooling_indicator = round(math.sqrt(tree.cooling_energy_indicator) / 6.75, 2)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# 5. Stock de carbone (t CO2)
|
|
||||||
botanical = getattr(tree, 'scientific_name', '') or getattr(tree, 'genus', '') or getattr(tree, 'name_fr', '')
|
|
||||||
if getattr(tree, 'carbon_stock', None) is None or overwrite_existing:
|
|
||||||
if c and h and c > 0 and h > 0:
|
|
||||||
k = get_genus_carbon_k(botanical)
|
|
||||||
tree.carbon_stock = round(k * c * h, 3)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# 6. Indice de biodiversité
|
|
||||||
if getattr(tree, 'biodiversity_index', None) is None or overwrite_existing:
|
|
||||||
tree.biodiversity_index = get_genus_biodiversity_index(botanical)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
return changed
|
|
||||||
|
|
@ -1411,11 +1411,6 @@ CATEGORY_ASSET_MAPPING = {
|
||||||
'LI_poles': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
'LI_poles': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
||||||
'publiclighting_luminaire': ('PublicLightingLuminaire', 'PublicLightingLuminaireModel'),
|
'publiclighting_luminaire': ('PublicLightingLuminaire', 'PublicLightingLuminaireModel'),
|
||||||
'LI_lightfixtures': ('PublicLightingLightFixture', 'PublicLightingLightFixtureModel'),
|
'LI_lightfixtures': ('PublicLightingLightFixture', 'PublicLightingLightFixtureModel'),
|
||||||
# ITS categories
|
|
||||||
'its_bollard_system': ('ITSBollardSystem', 'ITSAssetModel'),
|
|
||||||
'bollard_system': ('ITSBollardSystem', 'ITSAssetModel'),
|
|
||||||
'its_bollard_plot': ('ITSBollardPlot', 'ITSAssetModel'),
|
|
||||||
'bollard_plot': ('ITSBollardPlot', 'ITSAssetModel'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1861,18 +1856,6 @@ def create_asset_api(request):
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle bollard system (for bollard plots)
|
|
||||||
system_id = data.get('system_id') or data.get('bollard_system_id')
|
|
||||||
if system_id and _has_model_field(new_asset, 'system'):
|
|
||||||
from assets.models import ITSBollardSystem
|
|
||||||
try:
|
|
||||||
system_obj = ITSBollardSystem.objects.get(pk=system_id)
|
|
||||||
new_asset.system = system_obj
|
|
||||||
if not getattr(new_asset, 'location', None) and system_obj.location:
|
|
||||||
new_asset.location = system_obj.location
|
|
||||||
except ITSBollardSystem.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Set additional data
|
# Set additional data
|
||||||
extra_data = data.get('data', {})
|
extra_data = data.get('data', {})
|
||||||
if isinstance(extra_data, dict):
|
if isinstance(extra_data, dict):
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import os
|
|
||||||
import mimetypes
|
|
||||||
import json
|
import json
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
from django.http import JsonResponse, HttpResponseForbidden, FileResponse, Http404
|
from django.http import JsonResponse, HttpResponseForbidden
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _, get_language
|
from django.utils.translation import gettext as _, get_language
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
|
|
@ -354,62 +352,10 @@ def build_location_node(loc):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def publicbuilding_plan_file(request, pk):
|
|
||||||
"""
|
|
||||||
Sert directement le fichier de plan d'étage (SVG, PNG, JPG, PDF) associé à ce bâtiment ou niveau.
|
|
||||||
"""
|
|
||||||
from django.utils.encoding import smart_str
|
|
||||||
building = get_object_or_404(PublicBuilding, pk=pk)
|
|
||||||
|
|
||||||
file_field = None
|
|
||||||
if building.floor_plan and building.floor_plan.name:
|
|
||||||
file_field = building.floor_plan
|
|
||||||
else:
|
|
||||||
# Check direct floor document
|
|
||||||
doc = building.floor_documents.filter(document_type='floor_plan').first()
|
|
||||||
if not doc:
|
|
||||||
doc = building.documents.filter(document_type='floor_plan').first()
|
|
||||||
if doc and doc.file and doc.file.name:
|
|
||||||
file_field = doc.file
|
|
||||||
|
|
||||||
# Fallback to parent level if this is a room without direct plan
|
|
||||||
if not file_field and building.parent_location:
|
|
||||||
curr = building.parent_location
|
|
||||||
while curr and not file_field:
|
|
||||||
if curr.floor_plan and curr.floor_plan.name:
|
|
||||||
file_field = curr.floor_plan
|
|
||||||
break
|
|
||||||
pdoc = curr.floor_documents.filter(document_type='floor_plan').first()
|
|
||||||
if not pdoc:
|
|
||||||
pdoc = curr.documents.filter(document_type='floor_plan').first()
|
|
||||||
if pdoc and pdoc.file and pdoc.file.name:
|
|
||||||
file_field = pdoc.file
|
|
||||||
break
|
|
||||||
curr = curr.parent_location
|
|
||||||
|
|
||||||
if not file_field or not file_field.storage.exists(file_field.name):
|
|
||||||
raise Http404(_("Aucun plan d'étage disponible."))
|
|
||||||
|
|
||||||
content_type, _ = mimetypes.guess_type(file_field.name)
|
|
||||||
if not content_type:
|
|
||||||
if file_field.name.lower().endswith('.svg'):
|
|
||||||
content_type = 'image/svg+xml'
|
|
||||||
else:
|
|
||||||
content_type = 'application/octet-stream'
|
|
||||||
|
|
||||||
filename = os.path.basename(file_field.name)
|
|
||||||
response = FileResponse(file_field.open('rb'), content_type=content_type)
|
|
||||||
response['Content-Disposition'] = f'inline; filename="{smart_str(filename)}"'
|
|
||||||
response['X-Frame-Options'] = 'ALLOWALL'
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def building_detail(request, pk):
|
def building_detail(request, pk):
|
||||||
"""
|
"""
|
||||||
Vue détaillée d'une Localisation Bâtiment Public avec son arborescence unifiée,
|
Vue détaillée d'une Localisation Bâtiment Public avec son arborescence unifiée de localisations et d'assets physiques.
|
||||||
sa cartographie et sa navigation multi-niveaux / plans d'étage.
|
|
||||||
"""
|
"""
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from interventions.models import Intervention
|
from interventions.models import Intervention
|
||||||
|
|
@ -423,7 +369,7 @@ def building_detail(request, pk):
|
||||||
location_tree = build_location_node(building)
|
location_tree = build_location_node(building)
|
||||||
parent_location = building.parent_location
|
parent_location = building.parent_location
|
||||||
|
|
||||||
documents = building.documents.all().select_related('floor_location')
|
documents = building.documents.all()
|
||||||
|
|
||||||
content_type = ContentType.objects.get_for_model(PublicBuilding)
|
content_type = ContentType.objects.get_for_model(PublicBuilding)
|
||||||
|
|
||||||
|
|
@ -448,116 +394,6 @@ def building_detail(request, pk):
|
||||||
pass
|
pass
|
||||||
elif building.lon is not None and building.lat is not None:
|
elif building.lon is not None and building.lat is not None:
|
||||||
center_lon, center_lat = building.lon, building.lat
|
center_lon, center_lat = building.lon, building.lat
|
||||||
elif building.parent_location:
|
|
||||||
# Fallback to parent coords if room/floor without specific coords
|
|
||||||
curr_p = building.parent_location
|
|
||||||
while curr_p and (center_lon is None or center_lat is None):
|
|
||||||
if curr_p.lon is not None and curr_p.lat is not None:
|
|
||||||
center_lon, center_lat = curr_p.lon, curr_p.lat
|
|
||||||
break
|
|
||||||
elif curr_p.geom:
|
|
||||||
try:
|
|
||||||
g4326 = curr_p.geom.clone()
|
|
||||||
g4326.transform(4326)
|
|
||||||
center_lon, center_lat = g4326.centroid.x, g4326.centroid.y
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
curr_p = curr_p.parent_location
|
|
||||||
|
|
||||||
# Sécurisation des coordonnées JSON pour éviter les erreurs de syntaxe JS causées par la virgule en locale fr
|
|
||||||
center_lon_json = json.dumps(center_lon) if center_lon is not None else 'null'
|
|
||||||
center_lat_json = json.dumps(center_lat) if center_lat is not None else 'null'
|
|
||||||
|
|
||||||
# Détection du mode spécifique à un niveau / local
|
|
||||||
is_level = building.location_type in ('level', 'basement', 'attic', 'mezzanine')
|
|
||||||
is_sublocation = bool(building.parent_location is not None)
|
|
||||||
level_specific_mode = is_level or (is_sublocation and building.location_type not in ('site', 'building', 'wing'))
|
|
||||||
|
|
||||||
def serialize_floor_info(loc):
|
|
||||||
plan_url = loc.get_floor_plan_url()
|
|
||||||
rooms = []
|
|
||||||
for r in loc.child_locations.all():
|
|
||||||
rooms.append({
|
|
||||||
'id': r.id,
|
|
||||||
'code': r.code,
|
|
||||||
'name': r.name_fr or r.name or r.code,
|
|
||||||
'room_number': r.room_number or '',
|
|
||||||
'location_type_display': str(r.get_location_type_display()),
|
|
||||||
'url': r.get_absolute_url(),
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
'id': loc.id,
|
|
||||||
'code': loc.code,
|
|
||||||
'name': loc.name_fr or loc.name or loc.code,
|
|
||||||
'level_number': loc.level_number if loc.level_number is not None else 0,
|
|
||||||
'location_type': loc.location_type,
|
|
||||||
'location_type_display': str(loc.get_location_type_display()),
|
|
||||||
'surface_m2': float(loc.surface_m2 or loc.usable_surface_m2 or 0) or None,
|
|
||||||
'has_plan': bool(plan_url),
|
|
||||||
'plan_url': plan_url or '',
|
|
||||||
'url': loc.get_absolute_url(),
|
|
||||||
'rooms': rooms,
|
|
||||||
'rooms_count': len(rooms),
|
|
||||||
'assets_count': loc.location_assets.count(),
|
|
||||||
}
|
|
||||||
|
|
||||||
floors_data = []
|
|
||||||
active_floor = None
|
|
||||||
|
|
||||||
if level_specific_mode:
|
|
||||||
# Dans le détail d'un niveau, on ne retrouve que le plan lié, qui est directement affiché
|
|
||||||
if is_level or not building.parent_location:
|
|
||||||
target_floor = building
|
|
||||||
else:
|
|
||||||
target_floor = building
|
|
||||||
curr = building.parent_location
|
|
||||||
while curr:
|
|
||||||
if curr.location_type in ('level', 'basement', 'attic', 'mezzanine', 'wing'):
|
|
||||||
target_floor = curr
|
|
||||||
break
|
|
||||||
curr = curr.parent_location
|
|
||||||
active_floor_info = serialize_floor_info(target_floor)
|
|
||||||
floors_data = [active_floor_info]
|
|
||||||
active_floor = active_floor_info
|
|
||||||
else:
|
|
||||||
# Vue du bâtiment dans son ensemble: collecte de tous les niveaux
|
|
||||||
candidates = list(building.child_locations.filter(
|
|
||||||
Q(location_type__in=['level', 'basement', 'attic', 'mezzanine', 'wing', 'room']) |
|
|
||||||
Q(level_number__isnull=False)
|
|
||||||
).select_related('parent_location').prefetch_related('child_locations', 'location_assets', 'documents', 'floor_documents'))
|
|
||||||
|
|
||||||
for cand in list(candidates):
|
|
||||||
if cand.location_type == 'wing':
|
|
||||||
sub_levels = cand.child_locations.filter(
|
|
||||||
Q(location_type__in=['level', 'basement', 'attic', 'mezzanine', 'room']) |
|
|
||||||
Q(level_number__isnull=False)
|
|
||||||
).select_related('parent_location').prefetch_related('child_locations', 'location_assets', 'documents', 'floor_documents')
|
|
||||||
candidates.extend(list(sub_levels))
|
|
||||||
|
|
||||||
seen_ids = set()
|
|
||||||
unique_floors = []
|
|
||||||
for f in candidates:
|
|
||||||
if f.id not in seen_ids:
|
|
||||||
seen_ids.add(f.id)
|
|
||||||
unique_floors.append(f)
|
|
||||||
|
|
||||||
unique_floors.sort(key=lambda x: (x.level_number if x.level_number is not None else 99, x.code))
|
|
||||||
floors_data = [serialize_floor_info(f) for f in unique_floors]
|
|
||||||
|
|
||||||
if floors_data:
|
|
||||||
with_plan = [f for f in floors_data if f['has_plan']]
|
|
||||||
if with_plan:
|
|
||||||
active_floor = with_plan[0]
|
|
||||||
else:
|
|
||||||
level_0 = [f for f in floors_data if f['level_number'] == 0]
|
|
||||||
active_floor = level_0[0] if level_0 else floors_data[0]
|
|
||||||
elif building.get_floor_plan_url():
|
|
||||||
b_info = serialize_floor_info(building)
|
|
||||||
floors_data = [b_info]
|
|
||||||
active_floor = b_info
|
|
||||||
|
|
||||||
has_any_plan = any(f['has_plan'] for f in floors_data)
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'building': building,
|
'building': building,
|
||||||
|
|
@ -572,14 +408,6 @@ def building_detail(request, pk):
|
||||||
'building_geojson_json': json.dumps(building_geojson) if building_geojson else 'null',
|
'building_geojson_json': json.dumps(building_geojson) if building_geojson else 'null',
|
||||||
'center_lon': center_lon,
|
'center_lon': center_lon,
|
||||||
'center_lat': center_lat,
|
'center_lat': center_lat,
|
||||||
'center_lon_json': center_lon_json,
|
|
||||||
'center_lat_json': center_lat_json,
|
|
||||||
'floors_data': floors_data,
|
|
||||||
'floors_data_json': json.dumps(floors_data),
|
|
||||||
'active_floor': active_floor,
|
|
||||||
'active_floor_json': json.dumps(active_floor) if active_floor else 'null',
|
|
||||||
'level_specific_mode': level_specific_mode,
|
|
||||||
'has_any_plan': has_any_plan,
|
|
||||||
}
|
}
|
||||||
return render(request, 'assets/buildings/publicbuilding_detail.html', context)
|
return render(request, 'assets/buildings/publicbuilding_detail.html', context)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -461,8 +461,6 @@ ELEMENT_TYPE_LABELS = {
|
||||||
'controlcentergeoasset': _('Équipement CdC'),
|
'controlcentergeoasset': _('Équipement CdC'),
|
||||||
# --- ITS ---
|
# --- ITS ---
|
||||||
'itsgeoasset': _('Équipement ITS'),
|
'itsgeoasset': _('Équipement ITS'),
|
||||||
'itsbollardsystem': _('Système de bornes'),
|
|
||||||
'itsbollardplot': _('Plot de borne'),
|
|
||||||
# --- Propreté ---
|
# --- Propreté ---
|
||||||
'cleanlitterbin': _('Corbeille'),
|
'cleanlitterbin': _('Corbeille'),
|
||||||
'cleanglasscontainer': _('Conteneur verre'),
|
'cleanglasscontainer': _('Conteneur verre'),
|
||||||
|
|
@ -472,8 +470,6 @@ ELEMENT_TYPE_LABELS = {
|
||||||
# --- Espaces plein air ---
|
# --- Espaces plein air ---
|
||||||
'outdoorlocation': _('Espace public plein air'),
|
'outdoorlocation': _('Espace public plein air'),
|
||||||
'outdoorasset': _('Équipement plein air'),
|
'outdoorasset': _('Équipement plein air'),
|
||||||
# --- Zones de gestion ---
|
|
||||||
'managementzone': _('Zone de gestion'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -557,7 +553,6 @@ def build_thematic_config(thematics):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
management_zones_url = reverse('assets:management_zones_geojson')
|
|
||||||
result = {}
|
result = {}
|
||||||
for thematic in thematics:
|
for thematic in thematics:
|
||||||
code = thematic.code
|
code = thematic.code
|
||||||
|
|
@ -565,7 +560,6 @@ def build_thematic_config(thematics):
|
||||||
entry = dict(_THEMATIC_URLS[code])
|
entry = dict(_THEMATIC_URLS[code])
|
||||||
entry['name'] = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
|
entry['name'] = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
|
||||||
entry['icon'] = thematic.icon if hasattr(thematic, 'icon') else ''
|
entry['icon'] = thematic.icon if hasattr(thematic, 'icon') else ''
|
||||||
entry['management_zones_geojson_url'] = management_zones_url
|
|
||||||
result[code] = entry
|
result[code] = entry
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -681,7 +675,7 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
||||||
|
|
||||||
# Roads / public lighting / sign / artworks: municipality on locations (only if more than 1 option)
|
# Roads / public lighting / sign / artworks: municipality on locations (only if more than 1 option)
|
||||||
if len(municipality_options) > 1:
|
if len(municipality_options) > 1:
|
||||||
for thematic_code in ['roads', 'publiclighting', 'sign', 'artworks', 'nature']:
|
for thematic_code in ['roads', 'publiclighting', 'sign', 'artworks']:
|
||||||
if thematic_code in accessible_codes:
|
if thematic_code in accessible_codes:
|
||||||
config.setdefault(thematic_code, []).append({
|
config.setdefault(thematic_code, []).append({
|
||||||
'key': 'municipality',
|
'key': 'municipality',
|
||||||
|
|
@ -752,114 +746,7 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
||||||
|
|
||||||
# Nature
|
# Nature
|
||||||
if 'nature' in accessible_codes:
|
if 'nature' in accessible_codes:
|
||||||
from assets.models import NatureTree
|
config['nature'] = [
|
||||||
|
|
||||||
def _get_muni_region(m):
|
|
||||||
if not m:
|
|
||||||
return None
|
|
||||||
code = str(getattr(m, 'niscode', '') or '').strip()
|
|
||||||
if code.startswith('21'):
|
|
||||||
return 'BRU'
|
|
||||||
elif code[:2] in ('25', '51', '52', '53', '55', '56', '57', '61', '62', '63', '81', '82', '83', '84', '85', '91', '92', '93') or (code and code[0] in ('5', '6', '8', '9')):
|
|
||||||
return 'WAL'
|
|
||||||
elif (code and code[0] in ('1', '3', '4', '7')) or code[:2] in ('23', '24'):
|
|
||||||
return 'VLG'
|
|
||||||
name = (getattr(m, 'name_fr', '') or '').lower()
|
|
||||||
if any(b in name for b in ('bruxelles', 'watermael', 'boitsfort', 'berchem', 'ixelles', 'etterbeek', 'uccle', 'woluwe', 'schaerbeek', 'anderlecht', 'forest', 'jette', 'ganshoren', 'evere', 'saint-gilles', 'saint-josse', 'auderghem', 'molenbeek', 'koekelberg')):
|
|
||||||
return 'BRU'
|
|
||||||
return None
|
|
||||||
|
|
||||||
regional_labels = {
|
|
||||||
'BRU': _('Région (Bruxelles Mobilité)'),
|
|
||||||
'WAL': _('Région (Région wallonne)'),
|
|
||||||
'VLG': _('Région (Région flamande)'),
|
|
||||||
}
|
|
||||||
|
|
||||||
user_cfg = getattr(user, 'config', None)
|
|
||||||
user_is_admin = getattr(user, 'is_superuser', False) or (user_cfg and (user_cfg.has_role('admin') or user_cfg.has_role('top_manager')))
|
|
||||||
user_is_ext = (user_cfg is None) or (not user_cfg.is_intern)
|
|
||||||
user_must_limit = user_is_ext or (user_cfg and user_cfg.limit_assets_to_contracts)
|
|
||||||
|
|
||||||
visible_trees_qs = NatureTree.objects.exclude(status='removed')
|
|
||||||
if not user_is_admin and user_must_limit:
|
|
||||||
visible_trees_qs = visible_trees_qs.filter(location__municipality__in=municipalities)
|
|
||||||
|
|
||||||
tree_stats = list(visible_trees_qs.values('management_type', 'location__municipality_id').distinct())
|
|
||||||
has_trees = len(tree_stats) > 0
|
|
||||||
muni_list = list(municipalities)
|
|
||||||
management_type_options = []
|
|
||||||
|
|
||||||
if len(muni_list) == 1:
|
|
||||||
single_muni = muni_list[0]
|
|
||||||
reg = _get_muni_region(single_muni) or 'BRU'
|
|
||||||
reg_label = regional_labels.get(reg, _('Région'))
|
|
||||||
|
|
||||||
has_municipal = any(t['management_type'] == 'municipal' for t in tree_stats) if has_trees else True
|
|
||||||
has_regional = any(t['management_type'] == 'regional' for t in tree_stats) if has_trees else True
|
|
||||||
|
|
||||||
if has_municipal:
|
|
||||||
management_type_options.append({
|
|
||||||
'value': 'municipal',
|
|
||||||
'label': _('Commune (%(name)s)') % {'name': single_muni.name}
|
|
||||||
})
|
|
||||||
if has_regional:
|
|
||||||
management_type_options.append({
|
|
||||||
'value': 'regional',
|
|
||||||
'label': reg_label
|
|
||||||
})
|
|
||||||
elif len(muni_list) > 1:
|
|
||||||
muni_map = {m.id: m for m in muni_list}
|
|
||||||
|
|
||||||
# Municipal options for each municipality with municipal trees
|
|
||||||
for m in muni_list:
|
|
||||||
has_m_trees = any(
|
|
||||||
t['management_type'] == 'municipal' and t['location__municipality_id'] == m.id
|
|
||||||
for t in tree_stats
|
|
||||||
) if has_trees else True
|
|
||||||
if has_m_trees:
|
|
||||||
management_type_options.append({
|
|
||||||
'value': f'municipal_{m.id}',
|
|
||||||
'label': _('Commune (%(name)s)') % {'name': m.name}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Regional options grouped by region
|
|
||||||
regions_with_regional_trees = set()
|
|
||||||
for t in tree_stats:
|
|
||||||
if t['management_type'] == 'regional':
|
|
||||||
mid = t['location__municipality_id']
|
|
||||||
m_obj = muni_map.get(mid)
|
|
||||||
if m_obj:
|
|
||||||
reg = _get_muni_region(m_obj)
|
|
||||||
if reg:
|
|
||||||
regions_with_regional_trees.add(reg)
|
|
||||||
|
|
||||||
if not has_trees:
|
|
||||||
for m in muni_list:
|
|
||||||
reg = _get_muni_region(m)
|
|
||||||
if reg:
|
|
||||||
regions_with_regional_trees.add(reg)
|
|
||||||
|
|
||||||
for reg_code, reg_val in [('BRU', 'regional_bruxelles'), ('WAL', 'regional_wallonie'), ('VLG', 'regional_flandre')]:
|
|
||||||
if reg_code in regions_with_regional_trees:
|
|
||||||
management_type_options.append({
|
|
||||||
'value': reg_val,
|
|
||||||
'label': regional_labels[reg_code]
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
management_type_options = [
|
|
||||||
{'value': 'municipal', 'label': _('Communal')},
|
|
||||||
{'value': 'regional', 'label': _('Régional')},
|
|
||||||
]
|
|
||||||
|
|
||||||
nature_filters = []
|
|
||||||
if management_type_options:
|
|
||||||
nature_filters.append({
|
|
||||||
'key': 'management_type',
|
|
||||||
'label': _('Gestionnaire'),
|
|
||||||
'type': 'multi',
|
|
||||||
'options': management_type_options,
|
|
||||||
})
|
|
||||||
nature_filters.extend([
|
|
||||||
{
|
{
|
||||||
'key': 'location_type',
|
'key': 'location_type',
|
||||||
'label': _('Type de localisation'),
|
'label': _('Type de localisation'),
|
||||||
|
|
@ -875,8 +762,7 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
||||||
'type': 'checkbox',
|
'type': 'checkbox',
|
||||||
'value': '1',
|
'value': '1',
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
config['nature'] = nature_filters
|
|
||||||
|
|
||||||
# Water
|
# Water
|
||||||
if 'water' in accessible_codes:
|
if 'water' in accessible_codes:
|
||||||
|
|
@ -1050,7 +936,6 @@ def build_unified_add_modal_config(accessible_thematics, user=None):
|
||||||
from assets.permissions import (
|
from assets.permissions import (
|
||||||
can_edit_location_for_thematic,
|
can_edit_location_for_thematic,
|
||||||
get_allowed_actions_for_thematic,
|
get_allowed_actions_for_thematic,
|
||||||
can_edit_management_zones,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
location_type_by_thematic = {
|
location_type_by_thematic = {
|
||||||
|
|
@ -1063,7 +948,6 @@ def build_unified_add_modal_config(accessible_thematics, user=None):
|
||||||
|
|
||||||
asset_thematics = []
|
asset_thematics = []
|
||||||
location_thematics = []
|
location_thematics = []
|
||||||
management_zone_thematics = []
|
|
||||||
|
|
||||||
for thematic in accessible_thematics:
|
for thematic in accessible_thematics:
|
||||||
thematic_name = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
|
thematic_name = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
|
||||||
|
|
@ -1090,18 +974,9 @@ def build_unified_add_modal_config(accessible_thematics, user=None):
|
||||||
'location_label': location_meta['label'],
|
'location_label': location_meta['label'],
|
||||||
})
|
})
|
||||||
|
|
||||||
if can_edit_management_zones(user, thematic):
|
|
||||||
management_zone_thematics.append({
|
|
||||||
'code': thematic.code,
|
|
||||||
'name': thematic_name,
|
|
||||||
'icon': thematic_icon,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'asset_thematics': asset_thematics,
|
'asset_thematics': asset_thematics,
|
||||||
'location_thematics': location_thematics,
|
'location_thematics': location_thematics,
|
||||||
'management_zone_thematics': management_zone_thematics,
|
|
||||||
'management_zone_create_url': reverse('assets:management_zone_create'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from django.contrib.gis.db.models.functions import Transform
|
||||||
from django.contrib.gis.geos import GEOSGeometry, Point
|
from django.contrib.gis.geos import GEOSGeometry, Point
|
||||||
from django.contrib.gis.db.models.functions import Distance
|
from django.contrib.gis.db.models.functions import Distance
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q, Case, When, IntegerField, CharField, Count
|
from django.db.models import Q, Case, When, IntegerField, CharField
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.db.models import Func, Prefetch
|
from django.db.models import Func, Prefetch
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
@ -25,10 +25,7 @@ from collections import defaultdict
|
||||||
from common.utils import check_thematic_access
|
from common.utils import check_thematic_access
|
||||||
|
|
||||||
from common.models import Thematic, UserConfig, UserContractAccess, UserThematics
|
from common.models import Thematic, UserConfig, UserContractAccess, UserThematics
|
||||||
from assets.models import (
|
from assets.models import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, AssetCategory
|
||||||
ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, AssetCategory,
|
|
||||||
ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
|
||||||
)
|
|
||||||
from assets.permissions import can_edit_location_for_thematic, get_allowed_actions_for_thematic
|
from assets.permissions import can_edit_location_for_thematic, get_allowed_actions_for_thematic
|
||||||
|
|
||||||
from assets.forms import ITSLocationDocumentForm
|
from assets.forms import ITSLocationDocumentForm
|
||||||
|
|
@ -300,19 +297,11 @@ def its_assets_geojson(request):
|
||||||
|
|
||||||
if lon is None or lat is None:
|
if lon is None or lat is None:
|
||||||
location = getattr(obj, 'location', None)
|
location = getattr(obj, 'location', None)
|
||||||
system = getattr(obj, 'system', None)
|
|
||||||
if location is None and system is not None:
|
|
||||||
location = getattr(system, 'location', None)
|
|
||||||
if location is not None:
|
if location is not None:
|
||||||
if lon is None:
|
if lon is None:
|
||||||
lon = getattr(location, 'lon', None)
|
lon = getattr(location, 'lon', None)
|
||||||
if lat is None:
|
if lat is None:
|
||||||
lat = getattr(location, 'lat', None)
|
lat = getattr(location, 'lat', None)
|
||||||
if (lon is None or lat is None) and system is not None:
|
|
||||||
if lon is None:
|
|
||||||
lon = getattr(system, 'lon', None)
|
|
||||||
if lat is None:
|
|
||||||
lat = getattr(system, 'lat', None)
|
|
||||||
|
|
||||||
if geometry is None and lon is not None and lat is not None:
|
if geometry is None and lon is not None and lat is not None:
|
||||||
geometry = {
|
geometry = {
|
||||||
|
|
@ -324,13 +313,15 @@ def its_assets_geojson(request):
|
||||||
|
|
||||||
def obj_to_feature(obj, asset_type=None):
|
def obj_to_feature(obj, asset_type=None):
|
||||||
geometry, lon, lat = build_geometry_and_coords(obj)
|
geometry, lon, lat = build_geometry_and_coords(obj)
|
||||||
props = {
|
return {
|
||||||
|
"type": "Feature",
|
||||||
|
"id": obj.id,
|
||||||
|
"geometry": geometry,
|
||||||
|
"properties": {
|
||||||
"code": obj.code,
|
"code": obj.code,
|
||||||
"name": obj.get_name(),
|
"name": obj.get_name(),
|
||||||
"category": obj.category.get_name() if obj.category else None,
|
"category": obj.category.get_name() if obj.category else None,
|
||||||
"category_id": obj.category_id,
|
|
||||||
"model": obj.model.get_name() if obj.model else None,
|
"model": obj.model.get_name() if obj.model else None,
|
||||||
"model_id": obj.model_id,
|
|
||||||
"status": obj.status,
|
"status": obj.status,
|
||||||
"validation_status": obj.validation_status,
|
"validation_status": obj.validation_status,
|
||||||
"lon": lon,
|
"lon": lon,
|
||||||
|
|
@ -340,26 +331,7 @@ def its_assets_geojson(request):
|
||||||
"element_type": get_element_type_label(asset_type or ''),
|
"element_type": get_element_type_label(asset_type or ''),
|
||||||
"thematic": "its",
|
"thematic": "its",
|
||||||
"is_location": False,
|
"is_location": False,
|
||||||
}
|
},
|
||||||
if asset_type == "itsbollardsystem":
|
|
||||||
props["width"] = obj.width
|
|
||||||
props["direction"] = obj.get_direction_display() if hasattr(obj, 'get_direction_display') else obj.direction
|
|
||||||
props["plots_count"] = getattr(obj, 'plots_total', None)
|
|
||||||
if props["plots_count"] is None and hasattr(obj, 'plots'):
|
|
||||||
props["plots_count"] = obj.plots.count()
|
|
||||||
if obj.geom:
|
|
||||||
props["length"] = round(obj.geom.length, 2)
|
|
||||||
elif asset_type == "itsbollardplot":
|
|
||||||
props["position"] = obj.position
|
|
||||||
props["bollard_type"] = obj.get_bollard_type_display() if hasattr(obj, 'get_bollard_type_display') else obj.bollard_type
|
|
||||||
if getattr(obj, 'system', None):
|
|
||||||
props["system_code"] = obj.system.code
|
|
||||||
props["system_name"] = obj.system.get_name() if hasattr(obj.system, 'get_name') else (obj.system.name_fr or obj.system.name_nl or obj.system.code)
|
|
||||||
return {
|
|
||||||
"type": "Feature",
|
|
||||||
"id": obj.id,
|
|
||||||
"geometry": geometry,
|
|
||||||
"properties": props,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_assets_to_features(queryset, asset_type, features, feature_count):
|
def add_assets_to_features(queryset, asset_type, features, feature_count):
|
||||||
|
|
@ -376,35 +348,16 @@ def its_assets_geojson(request):
|
||||||
.select_related('location', 'category', 'model')
|
.select_related('location', 'category', 'model')
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
bollard_system_qs = (
|
|
||||||
ITSBollardSystem.objects
|
|
||||||
.select_related('location', 'category', 'model')
|
|
||||||
.annotate(plots_total=Count('plots'))
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
bollard_plot_qs = (
|
|
||||||
ITSBollardPlot.objects
|
|
||||||
.select_related('system', 'location', 'category', 'model')
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------- Filtres métier existants
|
# --------- Filtres métier existants
|
||||||
if its_location_ids:
|
if its_location_ids:
|
||||||
its_asset_qs = its_asset_qs.filter(location__id__in=its_location_ids)
|
its_asset_qs = its_asset_qs.filter(location__id__in=its_location_ids)
|
||||||
bollard_system_qs = bollard_system_qs.filter(location__id__in=its_location_ids)
|
|
||||||
bollard_plot_qs = bollard_plot_qs.filter(
|
|
||||||
Q(location__id__in=its_location_ids) | Q(system__location__id__in=its_location_ids)
|
|
||||||
)
|
|
||||||
|
|
||||||
if models:
|
if models:
|
||||||
its_asset_qs = its_asset_qs.filter(model__id__in=models)
|
its_asset_qs = its_asset_qs.filter(model__id__in=models)
|
||||||
bollard_system_qs = bollard_system_qs.filter(model__id__in=models)
|
|
||||||
bollard_plot_qs = bollard_plot_qs.filter(model__id__in=models)
|
|
||||||
|
|
||||||
if categories:
|
if categories:
|
||||||
its_asset_qs = its_asset_qs.filter(category__id__in=categories)
|
its_asset_qs = its_asset_qs.filter(category__id__in=categories)
|
||||||
bollard_system_qs = bollard_system_qs.filter(category__id__in=categories)
|
|
||||||
bollard_plot_qs = bollard_plot_qs.filter(category__id__in=categories)
|
|
||||||
|
|
||||||
if filter_geom:
|
if filter_geom:
|
||||||
its_asset_qs = its_asset_qs.filter(
|
its_asset_qs = its_asset_qs.filter(
|
||||||
|
|
@ -412,24 +365,11 @@ def its_assets_geojson(request):
|
||||||
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
||||||
Q(geom__isnull=True, location__isnull=True)
|
Q(geom__isnull=True, location__isnull=True)
|
||||||
)
|
)
|
||||||
bollard_system_qs = bollard_system_qs.filter(
|
|
||||||
Q(geom__intersects=filter_geom) |
|
|
||||||
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
|
||||||
Q(geom__isnull=True, location__isnull=True)
|
|
||||||
)
|
|
||||||
bollard_plot_qs = bollard_plot_qs.filter(
|
|
||||||
Q(geom__intersects=filter_geom) |
|
|
||||||
Q(geom__isnull=True, system__geom__intersects=filter_geom) |
|
|
||||||
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
|
||||||
Q(geom__isnull=True, system__isnull=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Recherche plein texte
|
# Recherche plein texte
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
from assets.views.core import apply_search
|
from assets.views.core import apply_search
|
||||||
its_asset_qs = apply_search(its_asset_qs, data)
|
its_asset_qs = apply_search(its_asset_qs, data)
|
||||||
bollard_system_qs = apply_search(bollard_system_qs, data)
|
|
||||||
bollard_plot_qs = apply_search(bollard_plot_qs, data)
|
|
||||||
|
|
||||||
# --------- Limitation pour utilisateurs externes
|
# --------- Limitation pour utilisateurs externes
|
||||||
user = request.user
|
user = request.user
|
||||||
|
|
@ -448,32 +388,26 @@ def its_assets_geojson(request):
|
||||||
can_view_assets=True
|
can_view_assets=True
|
||||||
).values_list('contract_id', flat=True)
|
).values_list('contract_id', flat=True)
|
||||||
|
|
||||||
# Ne garder que les assets qui ont un contrat actif auquel l'utilisateur a accès
|
# Ne garder que les assets qui ont un contrat actif
|
||||||
|
# auquel l'utilisateur a accès
|
||||||
its_asset_qs = its_asset_qs.filter(
|
its_asset_qs = its_asset_qs.filter(
|
||||||
its_assets_contracts__status='active',
|
its_assets_contracts__status='active',
|
||||||
its_assets_contracts__contract_id__in=accessible_contract_ids
|
its_assets_contracts__contract_id__in=accessible_contract_ids
|
||||||
).distinct()
|
).distinct()
|
||||||
bollard_system_qs = bollard_system_qs.filter(
|
|
||||||
bollard_systems_contracts__status='active',
|
|
||||||
bollard_systems_contracts__contract_id__in=accessible_contract_ids
|
|
||||||
).distinct()
|
|
||||||
bollard_plot_qs = bollard_plot_qs.filter(
|
|
||||||
system__bollard_systems_contracts__status='active',
|
|
||||||
system__bollard_systems_contracts__contract_id__in=accessible_contract_ids
|
|
||||||
).distinct()
|
|
||||||
else:
|
else:
|
||||||
# Non authentifié : ne rien exposer (plus sûr)
|
# Non authentifié : ne rien exposer (plus sûr)
|
||||||
its_asset_qs = its_asset_qs.none()
|
its_asset_qs = its_asset_qs.none()
|
||||||
bollard_system_qs = bollard_system_qs.none()
|
|
||||||
bollard_plot_qs = bollard_plot_qs.none()
|
# --------- Cap de sécurité
|
||||||
|
its_asset_qs = its_asset_qs[0:MAX_ASSET_FEATURES+1]
|
||||||
|
if len(its_asset_qs) >= MAX_ASSET_FEATURES:
|
||||||
|
return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413)
|
||||||
|
|
||||||
# --------- Construction GeoJSON
|
# --------- Construction GeoJSON
|
||||||
features = []
|
features = []
|
||||||
feature_count = 0
|
feature_count = 0
|
||||||
for qs, asset_type in [
|
for qs, asset_type in [
|
||||||
(its_asset_qs, "itsgeoasset"),
|
(its_asset_qs, "itsgeoasset"),
|
||||||
(bollard_system_qs, "itsbollardsystem"),
|
|
||||||
(bollard_plot_qs, "itsbollardplot"),
|
|
||||||
]:
|
]:
|
||||||
feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count)
|
feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count)
|
||||||
if limit_reached:
|
if limit_reached:
|
||||||
|
|
@ -669,9 +603,7 @@ def its_assets_autocomplete(request):
|
||||||
raw_query = raw_query_original.strip()
|
raw_query = raw_query_original.strip()
|
||||||
|
|
||||||
# CT ids (calculés une fois)
|
# CT ids (calculés une fois)
|
||||||
geoasset_ct_id = ContentType.objects.get_for_model(ITSGeoAsset).id
|
equipment_ct_id = ContentType.objects.get_for_model(ITSGeoAsset).id
|
||||||
bollard_system_ct_id = ContentType.objects.get_for_model(ITSBollardSystem).id
|
|
||||||
bollard_plot_ct_id = ContentType.objects.get_for_model(ITSBollardPlot).id
|
|
||||||
location_ct_id = ContentType.objects.get_for_model(ITSLocation).id
|
location_ct_id = ContentType.objects.get_for_model(ITSLocation).id
|
||||||
|
|
||||||
qn = strip_accents_lower(raw_query)
|
qn = strip_accents_lower(raw_query)
|
||||||
|
|
@ -694,36 +626,41 @@ def its_assets_autocomplete(request):
|
||||||
# en cas d'ID invalide ou autre, on ignore le filtre
|
# en cas d'ID invalide ou autre, on ignore le filtre
|
||||||
location_ids = None
|
location_ids = None
|
||||||
|
|
||||||
def query_model(model_class, ct_id, extra_select=()):
|
# Base queryset accent-insensible + actifs uniquement
|
||||||
q = (
|
qs = (
|
||||||
model_class.objects
|
ITSGeoAsset.objects
|
||||||
.annotate(
|
.annotate(
|
||||||
code_u=Lower(Unaccent('code')),
|
code_u=Lower(Unaccent('code')),
|
||||||
name_fr_u=Lower(Unaccent('name_fr')),
|
name_fr_u=Lower(Unaccent('name_fr')),
|
||||||
name_nl_u=Lower(Unaccent('name_nl')),
|
name_nl_u=Lower(Unaccent('name_nl')),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Si un terme de recherche est fourni, filtrer dessus
|
||||||
if has_search_term:
|
if has_search_term:
|
||||||
q = q.filter(
|
qs = qs.filter(
|
||||||
Q(code_u__contains=qn) |
|
Q(code_u__contains=qn) |
|
||||||
Q(name_fr_u__contains=qn) |
|
Q(name_fr_u__contains=qn) |
|
||||||
Q(name_nl_u__contains=qn)
|
Q(name_nl_u__contains=qn)
|
||||||
)
|
)
|
||||||
q = q.exclude(status='removed')
|
|
||||||
q = q.select_related('location', 'model', 'category', *extra_select)
|
qs = qs.exclude(status='removed') # <-- exclure les supprimés
|
||||||
|
qs = qs.select_related(
|
||||||
|
'location',
|
||||||
|
'model',
|
||||||
|
'category',
|
||||||
|
)
|
||||||
|
|
||||||
if location_ids:
|
if location_ids:
|
||||||
if hasattr(model_class, 'system'):
|
qs = qs.filter(location_id__in=location_ids)
|
||||||
q = q.filter(Q(location_id__in=location_ids) | Q(system__location_id__in=location_ids))
|
|
||||||
else:
|
|
||||||
q = q.filter(location_id__in=location_ids)
|
|
||||||
|
|
||||||
if category_id is not None:
|
if category_id is not None:
|
||||||
q = q.filter(
|
qs = qs.filter(
|
||||||
Q(category_id=category_id) |
|
Q(category_id=category_id) |
|
||||||
Q(category__isnull=True, model__category_id=category_id)
|
Q(category__isnull=True, model__category_id=category_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Priorité : exact > code startswith > nom startswith > contient
|
||||||
if has_search_term:
|
if has_search_term:
|
||||||
priority = Case(
|
priority = Case(
|
||||||
When(code_u=qn, then=0),
|
When(code_u=qn, then=0),
|
||||||
|
|
@ -736,29 +673,21 @@ def its_assets_autocomplete(request):
|
||||||
default=7,
|
default=7,
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
q = q.annotate(_prio=priority).order_by('_prio', 'code')[:50]
|
qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:100]
|
||||||
else:
|
else:
|
||||||
q = q.order_by('code')[:50]
|
# Sans terme de recherche, tri par code uniquement
|
||||||
|
qs = qs.order_by('code')[:100]
|
||||||
results = []
|
|
||||||
for obj in q:
|
|
||||||
results.append((obj, ct_id))
|
|
||||||
return results
|
|
||||||
|
|
||||||
all_results = (
|
|
||||||
query_model(ITSGeoAsset, geoasset_ct_id) +
|
|
||||||
query_model(ITSBollardSystem, bollard_system_ct_id) +
|
|
||||||
query_model(ITSBollardPlot, bollard_plot_ct_id, ('system', 'system__location'))
|
|
||||||
)
|
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for a, ct_id in all_results[:100]:
|
for a in qs:
|
||||||
|
# equipment_type depuis model/category
|
||||||
equipment_type = None
|
equipment_type = None
|
||||||
if getattr(a, 'model', None):
|
if getattr(a, 'model', None):
|
||||||
equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None)
|
equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None)
|
||||||
if not equipment_type and getattr(a, 'category', None):
|
if not equipment_type and getattr(a, 'category', None):
|
||||||
equipment_type = getattr(a.category, 'name', None)
|
equipment_type = getattr(a.category, 'name', None)
|
||||||
|
|
||||||
|
# coordonnées: asset > geom(4326) > location
|
||||||
lon = a.lon
|
lon = a.lon
|
||||||
lat = a.lat
|
lat = a.lat
|
||||||
|
|
||||||
|
|
@ -769,33 +698,24 @@ def its_assets_autocomplete(request):
|
||||||
g = a.geom.transform(4326, clone=True)
|
g = a.geom.transform(4326, clone=True)
|
||||||
coords = getattr(g, 'coords', None)
|
coords = getattr(g, 'coords', None)
|
||||||
if coords:
|
if coords:
|
||||||
if isinstance(coords[0], (int, float)):
|
|
||||||
if lon is None: lon = coords[0]
|
if lon is None: lon = coords[0]
|
||||||
if lat is None: lat = coords[1]
|
if lat is None: lat = coords[1]
|
||||||
elif isinstance(coords[0], (list, tuple)) and coords[0]:
|
|
||||||
# Pour LineString (ITSBollardSystem)
|
|
||||||
mid = len(coords) // 2
|
|
||||||
if lon is None: lon = coords[mid][0]
|
|
||||||
if lat is None: lat = coords[mid][1]
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
loc = getattr(a, 'location', None)
|
if (lon is None or lat is None) and getattr(a, 'location', None):
|
||||||
if loc is None and hasattr(a, 'system') and a.system:
|
if lon is None and getattr(a.location, 'lon', None) is not None:
|
||||||
loc = getattr(a.system, 'location', None)
|
lon = a.location.lon
|
||||||
|
if lat is None and getattr(a.location, 'lat', None) is not None:
|
||||||
|
lat = a.location.lat
|
||||||
|
|
||||||
if (lon is None or lat is None) and loc:
|
|
||||||
if lon is None and getattr(loc, 'lon', None) is not None:
|
|
||||||
lon = loc.lon
|
|
||||||
if lat is None and getattr(loc, 'lat', None) is not None:
|
|
||||||
lat = loc.lat
|
|
||||||
|
|
||||||
equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name()
|
equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name()
|
||||||
label = f"{a.code} - {equipment_label}" if a.code else equipment_label
|
label = f"{a.code} - {equipment_label}" if a.code else equipment_label
|
||||||
|
|
||||||
location_label = (
|
location_label = (
|
||||||
f"{loc.get_name()} ({loc.code})"
|
f"{a.location.get_name()} ({a.location.code})"
|
||||||
if loc else None
|
if getattr(a, "location", None) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
data.append({
|
data.append({
|
||||||
|
|
@ -805,9 +725,9 @@ def its_assets_autocomplete(request):
|
||||||
"equipment_id": a.id,
|
"equipment_id": a.id,
|
||||||
"equipment_label": equipment_label,
|
"equipment_label": equipment_label,
|
||||||
"equipment_type": equipment_type,
|
"equipment_type": equipment_type,
|
||||||
"equipment_ct": ct_id,
|
"equipment_ct": equipment_ct_id,
|
||||||
|
|
||||||
"location_id": loc.id if loc else None,
|
"location_id": a.location.id if getattr(a, "location", None) else None,
|
||||||
"location_label": location_label,
|
"location_label": location_label,
|
||||||
"location_ct": location_ct_id,
|
"location_ct": location_ct_id,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,427 +0,0 @@
|
||||||
import json
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.http import JsonResponse, HttpResponseForbidden
|
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views.decorators.http import require_GET, require_POST, require_http_methods
|
|
||||||
|
|
||||||
from common.models import Thematic, UserConfig
|
|
||||||
from assets.models import (
|
|
||||||
ManagementZone,
|
|
||||||
ManagementZoneType,
|
|
||||||
ManagementZoneDocument,
|
|
||||||
)
|
|
||||||
from assets.forms import ManagementZoneForm, ManagementZoneDocumentForm
|
|
||||||
from assets.permissions import (
|
|
||||||
can_view_management_zones,
|
|
||||||
can_edit_management_zones,
|
|
||||||
can_delete_management_zone,
|
|
||||||
get_editable_thematics_for_management_zones,
|
|
||||||
get_user_asset_thematic_access,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_GET
|
|
||||||
def management_zones_geojson(request):
|
|
||||||
"""
|
|
||||||
Retourne la collection GeoJSON des zones de gestion actives avec géométrie.
|
|
||||||
Filtre optionnel par ?thematic=<thematic_code>.
|
|
||||||
Vérifie que l'utilisateur a accès en lecture aux assets de la thématique.
|
|
||||||
"""
|
|
||||||
thematic_code = request.GET.get('thematic')
|
|
||||||
user = request.user
|
|
||||||
|
|
||||||
if thematic_code:
|
|
||||||
thematic = Thematic.objects.filter(code=thematic_code).first()
|
|
||||||
if not thematic:
|
|
||||||
return JsonResponse({'type': 'FeatureCollection', 'features': []})
|
|
||||||
|
|
||||||
can_view, _can_edit = get_user_asset_thematic_access(user, thematic)
|
|
||||||
if not can_view:
|
|
||||||
return HttpResponseForbidden("Access denied to this thematic management zones.")
|
|
||||||
|
|
||||||
zones_qs = ManagementZone.objects.filter(
|
|
||||||
thematic=thematic,
|
|
||||||
is_active=True,
|
|
||||||
geom__isnull=False
|
|
||||||
).select_related('thematic', 'zone_type')
|
|
||||||
else:
|
|
||||||
if user.is_superuser:
|
|
||||||
zones_qs = ManagementZone.objects.filter(is_active=True, geom__isnull=False)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
user_config = UserConfig.objects.get(user=user)
|
|
||||||
if user_config.roles.filter(name__in=['admin', 'operator', 'manager']).exists():
|
|
||||||
allowed_thematics = Thematic.objects.all()
|
|
||||||
else:
|
|
||||||
allowed_thematics = Thematic.objects.filter(
|
|
||||||
userthematics__user_config=user_config,
|
|
||||||
userthematics__can_view_assets=True
|
|
||||||
)
|
|
||||||
zones_qs = ManagementZone.objects.filter(
|
|
||||||
thematic__in=allowed_thematics,
|
|
||||||
is_active=True,
|
|
||||||
geom__isnull=False
|
|
||||||
)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
return JsonResponse({'type': 'FeatureCollection', 'features': []})
|
|
||||||
|
|
||||||
zones_qs = zones_qs.select_related('thematic', 'zone_type')
|
|
||||||
|
|
||||||
features = []
|
|
||||||
for zone in zones_qs:
|
|
||||||
geom_json = zone.geojson_geometry
|
|
||||||
if not geom_json:
|
|
||||||
continue
|
|
||||||
|
|
||||||
centroid = zone.geom.centroid.clone() if zone.geom else None
|
|
||||||
if centroid:
|
|
||||||
if centroid.srid and centroid.srid != 4326:
|
|
||||||
centroid.transform(4326)
|
|
||||||
lon = round(centroid.x, 7)
|
|
||||||
lat = round(centroid.y, 7)
|
|
||||||
else:
|
|
||||||
lon = None
|
|
||||||
lat = None
|
|
||||||
area = round(zone.geom.area, 1) if getattr(zone.geom, 'area', None) is not None else None
|
|
||||||
category_name = zone.zone_type.get_name() if zone.zone_type else ""
|
|
||||||
|
|
||||||
features.append({
|
|
||||||
'type': 'Feature',
|
|
||||||
'id': zone.id,
|
|
||||||
'properties': {
|
|
||||||
'id': zone.id,
|
|
||||||
'code': zone.code,
|
|
||||||
'name': zone.get_name(),
|
|
||||||
'thematic': zone.thematic.code,
|
|
||||||
'thematic_name': zone.thematic.name,
|
|
||||||
'zone_type_code': zone.zone_type.code if zone.zone_type else None,
|
|
||||||
'zone_type_name': category_name,
|
|
||||||
'category': category_name,
|
|
||||||
'color': zone.get_color(),
|
|
||||||
'instructions': zone.instructions or "",
|
|
||||||
'description': zone.description or "",
|
|
||||||
'seasonality_notes': zone.seasonality_notes or "",
|
|
||||||
'target_frequency_days': zone.target_frequency_days,
|
|
||||||
'status': 'active' if zone.is_active else 'inactive',
|
|
||||||
'element_type': str(_('Zone de gestion')),
|
|
||||||
'asset_type': 'managementzone',
|
|
||||||
'detail_url': reverse('assets:management_zone_detail', kwargs={'pk': zone.pk}),
|
|
||||||
'lon': lon,
|
|
||||||
'lat': lat,
|
|
||||||
'area': area,
|
|
||||||
'is_location': False,
|
|
||||||
'is_management_zone': True,
|
|
||||||
},
|
|
||||||
'geometry': geom_json
|
|
||||||
})
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
'type': 'FeatureCollection',
|
|
||||||
'features': features
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def management_zones_index(request):
|
|
||||||
"""
|
|
||||||
Page de listing et de consultation des zones de gestion avec filtres et carte de synthèse.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
if not can_view_management_zones(user):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas l'autorisation d'accéder aux zones de gestion."))
|
|
||||||
|
|
||||||
if user.is_superuser:
|
|
||||||
accessible_thematics = Thematic.objects.all()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
user_config = UserConfig.objects.get(user=user)
|
|
||||||
if user_config.roles.filter(name__in=['admin', 'operator', 'manager']).exists():
|
|
||||||
accessible_thematics = Thematic.objects.all()
|
|
||||||
else:
|
|
||||||
accessible_thematics = Thematic.objects.filter(
|
|
||||||
userthematics__user_config=user_config,
|
|
||||||
userthematics__can_view_assets=True
|
|
||||||
)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
accessible_thematics = Thematic.objects.none()
|
|
||||||
|
|
||||||
zones_qs = ManagementZone.objects.filter(
|
|
||||||
thematic__in=accessible_thematics
|
|
||||||
).select_related('thematic', 'zone_type', 'created_by')
|
|
||||||
|
|
||||||
# Filtre par thématique
|
|
||||||
thematic_code = request.GET.get('thematic', '').strip()
|
|
||||||
selected_thematic = None
|
|
||||||
if thematic_code:
|
|
||||||
selected_thematic = accessible_thematics.filter(code=thematic_code).first()
|
|
||||||
if selected_thematic:
|
|
||||||
zones_qs = zones_qs.filter(thematic=selected_thematic)
|
|
||||||
|
|
||||||
# Filtre par type
|
|
||||||
zone_type_id = request.GET.get('zone_type', '').strip()
|
|
||||||
if zone_type_id:
|
|
||||||
zones_qs = zones_qs.filter(zone_type_id=zone_type_id)
|
|
||||||
|
|
||||||
# Filtre par statut actif/inactif
|
|
||||||
status_filter = request.GET.get('status', '').strip()
|
|
||||||
if status_filter == 'active':
|
|
||||||
zones_qs = zones_qs.filter(is_active=True)
|
|
||||||
elif status_filter == 'inactive':
|
|
||||||
zones_qs = zones_qs.filter(is_active=False)
|
|
||||||
|
|
||||||
# Recherche plein texte
|
|
||||||
q = request.GET.get('q', '').strip()
|
|
||||||
if q:
|
|
||||||
from django.db.models import Q
|
|
||||||
zones_qs = zones_qs.filter(
|
|
||||||
Q(code__icontains=q) |
|
|
||||||
Q(name_fr__icontains=q) |
|
|
||||||
Q(name_nl__icontains=q) |
|
|
||||||
Q(description__icontains=q) |
|
|
||||||
Q(instructions__icontains=q) |
|
|
||||||
Q(seasonality_notes__icontains=q)
|
|
||||||
)
|
|
||||||
|
|
||||||
zones_qs = zones_qs.order_by('thematic__name_fr', 'code')
|
|
||||||
|
|
||||||
# Types disponibles pour le filtre
|
|
||||||
if selected_thematic:
|
|
||||||
available_types = ManagementZoneType.objects.filter(thematic=selected_thematic)
|
|
||||||
else:
|
|
||||||
available_types = ManagementZoneType.objects.filter(thematic__in=accessible_thematics)
|
|
||||||
|
|
||||||
can_create = can_edit_management_zones(user)
|
|
||||||
editable_thematics = get_editable_thematics_for_management_zones(user)
|
|
||||||
|
|
||||||
zones_list = []
|
|
||||||
for zone in zones_qs:
|
|
||||||
zones_list.append({
|
|
||||||
'zone': zone,
|
|
||||||
'can_edit': can_edit_management_zones(user, thematic=zone.thematic)
|
|
||||||
})
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'zones_list': zones_list,
|
|
||||||
'accessible_thematics': accessible_thematics,
|
|
||||||
'selected_thematic': selected_thematic,
|
|
||||||
'selected_thematic_code': thematic_code,
|
|
||||||
'available_types': available_types,
|
|
||||||
'selected_type_id': zone_type_id,
|
|
||||||
'selected_status': status_filter,
|
|
||||||
'search_query': q,
|
|
||||||
'can_create': can_create,
|
|
||||||
'editable_thematics': editable_thematics,
|
|
||||||
'total_count': zones_qs.count(),
|
|
||||||
}
|
|
||||||
return render(request, 'assets/management_zones/management_zones_index.html', context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def management_zone_create(request):
|
|
||||||
"""
|
|
||||||
Création d'une nouvelle zone de gestion avec tracé géographique sur la carte.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
if not can_edit_management_zones(user):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas l'autorisation de créer des zones de gestion."))
|
|
||||||
|
|
||||||
editable_thematics = get_editable_thematics_for_management_zones(user)
|
|
||||||
if not editable_thematics.exists():
|
|
||||||
return HttpResponseForbidden(_("Aucune thématique disponible pour la création de zones."))
|
|
||||||
|
|
||||||
initial = {}
|
|
||||||
thematic_code = request.GET.get('thematic')
|
|
||||||
if thematic_code:
|
|
||||||
thematic = editable_thematics.filter(code=thematic_code).first()
|
|
||||||
if thematic:
|
|
||||||
initial['thematic'] = thematic
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = ManagementZoneForm(request.POST, user=user)
|
|
||||||
if form.is_valid():
|
|
||||||
target_thematic = form.cleaned_data['thematic']
|
|
||||||
if not can_edit_management_zones(user, thematic=target_thematic):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas le droit de créer une zone pour cette thématique."))
|
|
||||||
zone = form.save(commit=False)
|
|
||||||
zone.created_by = user
|
|
||||||
zone.save()
|
|
||||||
messages.success(request, _("La zone de gestion « %(code)s » a été créée avec succès.") % {'code': zone.code})
|
|
||||||
return redirect('assets:management_zone_detail', pk=zone.pk)
|
|
||||||
else:
|
|
||||||
form = ManagementZoneForm(user=user, initial=initial)
|
|
||||||
|
|
||||||
return render(request, 'assets/management_zones/management_zones_form.html', {
|
|
||||||
'form': form,
|
|
||||||
'is_create': True,
|
|
||||||
'title': _("Nouvelle zone de gestion"),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def management_zone_edit(request, pk):
|
|
||||||
"""
|
|
||||||
Modification d'une zone de gestion existante et de sa géométrie.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
zone = get_object_or_404(ManagementZone.objects.select_related('thematic'), pk=pk)
|
|
||||||
|
|
||||||
if not can_edit_management_zones(user, thematic=zone.thematic):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas l'autorisation de modifier cette zone de gestion."))
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = ManagementZoneForm(request.POST, instance=zone, user=user)
|
|
||||||
if form.is_valid():
|
|
||||||
target_thematic = form.cleaned_data['thematic']
|
|
||||||
if not can_edit_management_zones(user, thematic=target_thematic):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas le droit d'assigner cette zone à cette thématique."))
|
|
||||||
form.save()
|
|
||||||
messages.success(request, _("La zone de gestion « %(code)s » a été mise à jour.") % {'code': zone.code})
|
|
||||||
return redirect('assets:management_zone_detail', pk=zone.pk)
|
|
||||||
else:
|
|
||||||
form = ManagementZoneForm(instance=zone, user=user)
|
|
||||||
|
|
||||||
return render(request, 'assets/management_zones/management_zones_form.html', {
|
|
||||||
'form': form,
|
|
||||||
'zone': zone,
|
|
||||||
'is_create': False,
|
|
||||||
'title': _("Modifier la zone de gestion %(code)s") % {'code': zone.code},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def management_zone_detail(request, pk):
|
|
||||||
"""
|
|
||||||
Fiche détaillée d'une zone de gestion : informations, consignes, calendrier, carte, documents et équipements.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
zone = get_object_or_404(
|
|
||||||
ManagementZone.objects.select_related('thematic', 'zone_type', 'created_by'),
|
|
||||||
pk=pk
|
|
||||||
)
|
|
||||||
|
|
||||||
if not can_view_management_zones(user, thematic=zone.thematic):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas l'autorisation de consulter cette zone de gestion."))
|
|
||||||
|
|
||||||
can_edit = can_edit_management_zones(user, thematic=zone.thematic)
|
|
||||||
documents = zone.documents.select_related('uploaded_by').order_by('-upload_date')
|
|
||||||
explicit_assets = zone.explicit_assets.select_related('content_type').all()
|
|
||||||
doc_form = ManagementZoneDocumentForm() if can_edit else None
|
|
||||||
|
|
||||||
# Calcul de superficie indicative si géométrie présente (SRID 3812 en mètres)
|
|
||||||
area_m2 = None
|
|
||||||
area_ha = None
|
|
||||||
if zone.geom:
|
|
||||||
try:
|
|
||||||
area_m2 = round(zone.geom.area, 1)
|
|
||||||
area_ha = round(area_m2 / 10000, 2)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return render(request, 'assets/management_zones/management_zones_detail.html', {
|
|
||||||
'zone': zone,
|
|
||||||
'can_edit': can_edit,
|
|
||||||
'documents': documents,
|
|
||||||
'explicit_assets': explicit_assets,
|
|
||||||
'doc_form': doc_form,
|
|
||||||
'area_m2': area_m2,
|
|
||||||
'area_ha': area_ha,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_http_methods(["GET", "POST"])
|
|
||||||
def management_zone_delete(request, pk):
|
|
||||||
"""
|
|
||||||
Suppression d'une zone de gestion.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
zone = get_object_or_404(ManagementZone.objects.select_related('thematic'), pk=pk)
|
|
||||||
|
|
||||||
if not can_delete_management_zone(user, zone):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas l'autorisation de supprimer cette zone de gestion."))
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
code = zone.code
|
|
||||||
thematic_code = zone.thematic.code
|
|
||||||
zone.delete()
|
|
||||||
messages.success(request, _("La zone de gestion « %(code)s » a été supprimée.") % {'code': code})
|
|
||||||
return redirect(f"{reverse('assets:management_zones_index')}?thematic={thematic_code}")
|
|
||||||
|
|
||||||
return render(request, 'assets/management_zones/management_zones_confirm_delete.html', {
|
|
||||||
'zone': zone,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def management_zone_add_document(request, pk):
|
|
||||||
"""
|
|
||||||
Ajout de documents techniques ou de consignes à la zone de gestion.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
zone = get_object_or_404(ManagementZone.objects.select_related('thematic'), pk=pk)
|
|
||||||
|
|
||||||
if not can_edit_management_zones(user, thematic=zone.thematic):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas l'autorisation d'ajouter des documents à cette zone."))
|
|
||||||
|
|
||||||
form = ManagementZoneDocumentForm(request.POST, request.FILES)
|
|
||||||
if form.is_valid():
|
|
||||||
files = request.FILES.getlist('documents')
|
|
||||||
for f in files:
|
|
||||||
ManagementZoneDocument.objects.create(
|
|
||||||
zone=zone,
|
|
||||||
file=f,
|
|
||||||
uploaded_by=user
|
|
||||||
)
|
|
||||||
messages.success(request, _("%(count)d document(s) ajouté(s) avec succès.") % {'count': len(files)})
|
|
||||||
else:
|
|
||||||
messages.error(request, _("Erreur lors de l'ajout des documents."))
|
|
||||||
|
|
||||||
return redirect('assets:management_zone_detail', pk=zone.pk)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def management_zone_delete_document(request, pk, document_id):
|
|
||||||
"""
|
|
||||||
Suppression d'un document attaché à la zone de gestion.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
zone = get_object_or_404(ManagementZone.objects.select_related('thematic'), pk=pk)
|
|
||||||
|
|
||||||
if not can_edit_management_zones(user, thematic=zone.thematic):
|
|
||||||
return HttpResponseForbidden(_("Vous n'avez pas l'autorisation de supprimer ce document."))
|
|
||||||
|
|
||||||
doc = get_object_or_404(ManagementZoneDocument, pk=document_id, zone=zone)
|
|
||||||
doc.delete()
|
|
||||||
messages.success(request, _("Le document a été supprimé avec succès."))
|
|
||||||
return redirect('assets:management_zone_detail', pk=zone.pk)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_GET
|
|
||||||
def management_zone_types_ajax(request):
|
|
||||||
"""
|
|
||||||
Endpoint JSON retournant les types de zones pour une thématique donnée.
|
|
||||||
"""
|
|
||||||
thematic_id = request.GET.get('thematic_id')
|
|
||||||
thematic_code = request.GET.get('thematic_code')
|
|
||||||
qs = ManagementZoneType.objects.all()
|
|
||||||
if thematic_id:
|
|
||||||
qs = qs.filter(thematic_id=thematic_id)
|
|
||||||
elif thematic_code:
|
|
||||||
qs = qs.filter(thematic__code=thematic_code)
|
|
||||||
types = [
|
|
||||||
{
|
|
||||||
'id': t.id,
|
|
||||||
'name': t.get_name(),
|
|
||||||
'code': t.code,
|
|
||||||
'color': t.default_color
|
|
||||||
}
|
|
||||||
for t in qs
|
|
||||||
]
|
|
||||||
return JsonResponse({'types': types})
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.http import JsonResponse, Http404
|
from django.http import JsonResponse, Http404
|
||||||
from django.contrib.gis.db.models.functions import Transform
|
from django.contrib.gis.db.models.functions import Transform
|
||||||
from django.contrib.gis.geos import GEOSGeometry, Point, MultiPolygon
|
from django.contrib.gis.geos import GEOSGeometry, Point, MultiPolygon
|
||||||
from django.contrib.gis.db.models.functions import Distance
|
from django.contrib.gis.db.models.functions import Distance
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import get_language, gettext as _
|
from django.utils.translation import get_language, gettext as _
|
||||||
from django.db.models import F, Func, Q, Case, When, CharField, IntegerField, Value, Sum, Count
|
from django.db.models import F, Func, Q, Case, When, CharField, IntegerField, Value
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.views.decorators.http import require_GET
|
from django.views.decorators.http import require_GET
|
||||||
|
|
@ -23,20 +23,94 @@ from common.models import UserThematics
|
||||||
from assets.models import NatureLocation, NatureGreenSurface, NatureAssetModel, NatureTree, NatureShrub, NatureBench, AssetCategory, NatureRWIZ, NatureRWIAsset
|
from assets.models import NatureLocation, NatureGreenSurface, NatureAssetModel, NatureTree, NatureShrub, NatureBench, AssetCategory, NatureRWIZ, NatureRWIAsset
|
||||||
from assets.models import GreenSurfacesComment, GreenSurfacesCommentPhoto
|
from assets.models import GreenSurfacesComment, GreenSurfacesCommentPhoto
|
||||||
|
|
||||||
from interventions.models import Intervention, InterventionLocation, InterventionAsset
|
from interventions.models import InterventionLocation
|
||||||
from interventions.views.drafts import InterventionDraftManager
|
from interventions.views.drafts import InterventionDraftManager
|
||||||
|
|
||||||
@check_thematic_access('nature')
|
@check_thematic_access('nature')
|
||||||
def nature_index(request):
|
def nature_index(request):
|
||||||
query_params = request.GET.copy()
|
thematic = 'nature'
|
||||||
if 'thematic' not in query_params:
|
lang = get_language()
|
||||||
query_params['thematic'] = 'nature'
|
|
||||||
return redirect(f"{reverse('assets:index')}?{query_params.urlencode()}")
|
# Get filters from URL
|
||||||
|
statuses = request.GET.getlist('status')
|
||||||
|
highway = request.GET.get('highway') == "1"
|
||||||
|
selected_categories = request.GET.getlist('category')
|
||||||
|
selected_models = request.GET.getlist('model')
|
||||||
|
selected_location_type = request.GET.get('location_type', '')
|
||||||
|
|
||||||
|
# Get preventive draft ID (if any) for selecting assets
|
||||||
|
preventive_draft_id = request.GET.get('preventive_draft_id')
|
||||||
|
if preventive_draft_id:
|
||||||
|
drafts = InterventionDraftManager(request.session)
|
||||||
|
draft_data = drafts.get(preventive_draft_id)
|
||||||
|
form_data = draft_data.get('form_data', {})
|
||||||
|
preventive_element_type = form_data.get('type_of_selected_elements')
|
||||||
|
if preventive_element_type == 'assets':
|
||||||
|
selected_assets = draft_data.get('assets', []) or []
|
||||||
|
else:
|
||||||
|
selected_locations_ids = draft_data.get('locations', [])
|
||||||
|
|
||||||
|
|
||||||
|
thematic_choices_qs = (
|
||||||
|
UserThematics.objects
|
||||||
|
.filter(user_config=request.user.config, can_view_assets=True)
|
||||||
|
.select_related('thematic')
|
||||||
|
.order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}')
|
||||||
|
)
|
||||||
|
thematics = [tc.thematic for tc in thematic_choices_qs]
|
||||||
|
|
||||||
|
|
||||||
|
selected_thematic = {
|
||||||
|
"code": thematic,
|
||||||
|
"name": next((t.name for t in thematics if t.code == thematic), None),
|
||||||
|
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
categories = list(AssetCategory.objects.filter(thematic__code = 'nature'))
|
||||||
|
categories.sort(key=lambda m: m.get_name().lower())
|
||||||
|
category_choices = [(m.id, m.get_name()) for m in categories]
|
||||||
|
|
||||||
|
models = list(NatureAssetModel.objects.all())
|
||||||
|
models.sort(key=lambda m: (m.get_name() or "").lower())
|
||||||
|
model_choices = [(m.id, m.get_name()) for m in models]
|
||||||
|
|
||||||
|
category_model_map = defaultdict(list)
|
||||||
|
models = NatureAssetModel.objects.all()
|
||||||
|
for model in models:
|
||||||
|
if model.category:
|
||||||
|
display_name = model.get_name()
|
||||||
|
category_model_map[str(model.category_id)].append({
|
||||||
|
"key": str(model.id),
|
||||||
|
"label": display_name,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'thematics': thematics,
|
||||||
|
'category_choices': category_choices,
|
||||||
|
'model_choices': model_choices,
|
||||||
|
'category_model_map': dict(category_model_map),
|
||||||
|
'selected_thematic': selected_thematic,
|
||||||
|
'selected_statuses': statuses,
|
||||||
|
'selected_categories': selected_categories,
|
||||||
|
'selected_models': selected_models,
|
||||||
|
'highway': highway,
|
||||||
|
'selected_location_type': selected_location_type,
|
||||||
|
'preventive_draft_id': preventive_draft_id,
|
||||||
|
'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '',
|
||||||
|
'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [],
|
||||||
|
"selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [],
|
||||||
|
'locations_tab_title': _("Voiries"),
|
||||||
|
'locations_assets_title': "Assets",
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "assets/nature_index.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def nature_locations(request):
|
def nature_locations(request):
|
||||||
return redirect(f"{reverse('assets:index')}?thematic=nature")
|
pass
|
||||||
|
|
||||||
|
|
||||||
@check_thematic_access('nature')
|
@check_thematic_access('nature')
|
||||||
|
|
@ -44,304 +118,11 @@ def nature_locations_detail(request, location_id):
|
||||||
section = get_object_or_404(NatureLocation, id=location_id)
|
section = get_object_or_404(NatureLocation, id=location_id)
|
||||||
content_type = ContentType.objects.get_for_model(section)
|
content_type = ContentType.objects.get_for_model(section)
|
||||||
|
|
||||||
# 1. Direct assets linked to this location (evaluated once)
|
interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=content_type, object_id=section.id).select_related('intervention').order_by('-intervention__creation_time')]
|
||||||
trees = list(section.nature_trees.select_related('model', 'category').all())
|
|
||||||
green_surfaces = list(section.nature_green_surfaces.select_related('model', 'category').all())
|
|
||||||
shrubs = list(section.nature_shrubs.select_related('model', 'category').all())
|
|
||||||
benches = list(section.nature_benches.select_related('model', 'category').all())
|
|
||||||
|
|
||||||
# 2. Key figures & metrics
|
|
||||||
trees_count = len(trees)
|
|
||||||
trees_active_count = sum(1 for t in trees if getattr(t, 'status', None) == 'active')
|
|
||||||
trees_remarkable_count = sum(1 for t in trees if getattr(t, 'is_remarkable', False))
|
|
||||||
|
|
||||||
green_surfaces_count = len(green_surfaces)
|
|
||||||
total_green_surface = sum((gs.surface_area or 0.0) for gs in green_surfaces)
|
|
||||||
|
|
||||||
def is_massif_arbore(gs):
|
|
||||||
if not getattr(gs, 'model', None):
|
|
||||||
return False
|
|
||||||
n_fr = (gs.model.name_fr or '').lower()
|
|
||||||
n_nl = (gs.model.name_nl or '').lower()
|
|
||||||
code = (gs.model.code or '').upper()
|
|
||||||
return (
|
|
||||||
'arboré' in n_fr or 'arbore' in n_fr or
|
|
||||||
'boom' in n_nl or 'hout' in n_nl or
|
|
||||||
'ARBORE' in code
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_pre_fleuri(gs):
|
|
||||||
if not getattr(gs, 'model', None):
|
|
||||||
return False
|
|
||||||
n_fr = (gs.model.name_fr or '').lower()
|
|
||||||
n_nl = (gs.model.name_nl or '').lower()
|
|
||||||
code = (gs.model.code or '').upper()
|
|
||||||
return (
|
|
||||||
'fleuri' in n_fr or 'fleur' in n_fr or
|
|
||||||
'bloem' in n_nl or
|
|
||||||
'FLEURI' in code
|
|
||||||
)
|
|
||||||
|
|
||||||
surface_massif_arbore = sum((gs.surface_area or 0.0) for gs in green_surfaces if is_massif_arbore(gs))
|
|
||||||
massif_arbore_count = sum(1 for gs in green_surfaces if is_massif_arbore(gs))
|
|
||||||
|
|
||||||
surface_pre_fleuri = sum((gs.surface_area or 0.0) for gs in green_surfaces if is_pre_fleuri(gs))
|
|
||||||
pre_fleuri_count = sum(1 for gs in green_surfaces if is_pre_fleuri(gs))
|
|
||||||
|
|
||||||
# Breakdown by green surface typology
|
|
||||||
type_map = {}
|
|
||||||
for gs in green_surfaces:
|
|
||||||
m_id = gs.model_id or 0
|
|
||||||
if m_id not in type_map:
|
|
||||||
m_name = gs.model.get_name() if getattr(gs, 'model', None) else str(_("Autre / Non défini"))
|
|
||||||
m_color = (gs.model.color if (getattr(gs, 'model', None) and gs.model.color) else '#4caf50')
|
|
||||||
type_map[m_id] = {
|
|
||||||
'name': m_name,
|
|
||||||
'color': m_color,
|
|
||||||
'count': 0,
|
|
||||||
'total_area': 0.0,
|
|
||||||
}
|
|
||||||
type_map[m_id]['count'] += 1
|
|
||||||
type_map[m_id]['total_area'] += (gs.surface_area or 0.0)
|
|
||||||
|
|
||||||
surfaces_by_type = sorted(type_map.values(), key=lambda x: x['total_area'], reverse=True)
|
|
||||||
for item in surfaces_by_type:
|
|
||||||
item['percent'] = round((item['total_area'] / total_green_surface * 100), 1) if total_green_surface > 0 else 0
|
|
||||||
|
|
||||||
shrubs_count = len(shrubs)
|
|
||||||
benches_count = len(benches)
|
|
||||||
total_assets_count = trees_count + green_surfaces_count + shrubs_count + benches_count
|
|
||||||
|
|
||||||
# 3. Interventions linked to location + linked to assets on this location
|
|
||||||
location_intervention_ids = set(InterventionLocation.objects.filter(
|
|
||||||
content_type=content_type, object_id=section.id
|
|
||||||
).values_list('intervention_id', flat=True))
|
|
||||||
|
|
||||||
tree_ids = [t.id for t in trees]
|
|
||||||
gs_ids = [gs.id for gs in green_surfaces]
|
|
||||||
shrub_ids = [s.id for s in shrubs]
|
|
||||||
bench_ids = [b.id for b in benches]
|
|
||||||
|
|
||||||
asset_tuples = [
|
|
||||||
(ContentType.objects.get_for_model(NatureTree), tree_ids),
|
|
||||||
(ContentType.objects.get_for_model(NatureGreenSurface), gs_ids),
|
|
||||||
(ContentType.objects.get_for_model(NatureShrub), shrub_ids),
|
|
||||||
(ContentType.objects.get_for_model(NatureBench), bench_ids),
|
|
||||||
]
|
|
||||||
for ct, ids in asset_tuples:
|
|
||||||
if ids:
|
|
||||||
int_ids = InterventionAsset.objects.filter(
|
|
||||||
content_type=ct, object_id__in=ids
|
|
||||||
).values_list('intervention_id', flat=True)
|
|
||||||
location_intervention_ids.update(int_ids)
|
|
||||||
|
|
||||||
interventions = list(Intervention.objects.filter(
|
|
||||||
id__in=location_intervention_ids
|
|
||||||
).select_related('contract').order_by('-creation_time'))
|
|
||||||
|
|
||||||
# 4. Documents
|
|
||||||
documents = []
|
|
||||||
try:
|
|
||||||
from documents.models import DocumentAttachment
|
|
||||||
attachments = DocumentAttachment.objects.filter(
|
|
||||||
content_type=content_type, object_id=section.id
|
|
||||||
).select_related('document').prefetch_related('document__versions')
|
|
||||||
for att in attachments:
|
|
||||||
doc = att.document
|
|
||||||
latest_version = doc.latest_version if hasattr(doc, 'latest_version') else None
|
|
||||||
documents.append({
|
|
||||||
'title': doc.title or getattr(latest_version, 'filename', str(doc)),
|
|
||||||
'date': doc.created_at,
|
|
||||||
'tag': doc.tag.name_fr if (hasattr(doc, 'tag') and doc.tag) else '',
|
|
||||||
'url': latest_version.file.url if (latest_version and latest_version.file) else None,
|
|
||||||
'thumbnail_url': latest_version.thumbnail.url if (latest_version and getattr(latest_version, 'thumbnail', None)) else None,
|
|
||||||
})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if hasattr(section, 'documents'):
|
|
||||||
for doc in section.documents.all():
|
|
||||||
documents.append({
|
|
||||||
'title': doc.file.name if hasattr(doc, 'file') and doc.file else str(doc),
|
|
||||||
'date': getattr(doc, 'upload_date', None),
|
|
||||||
'tag': '',
|
|
||||||
'url': doc.file.url if (hasattr(doc, 'file') and doc.file) else None,
|
|
||||||
'thumbnail_url': doc.thumbnail.url if (hasattr(doc, 'thumbnail') and doc.thumbnail) else None,
|
|
||||||
})
|
|
||||||
|
|
||||||
# 5. Serialized GeoJSON for map display
|
|
||||||
assets_features = []
|
|
||||||
|
|
||||||
for gs in green_surfaces:
|
|
||||||
geom = None
|
|
||||||
if gs.geojson:
|
|
||||||
try:
|
|
||||||
geom = json.loads(gs.geojson)
|
|
||||||
except Exception:
|
|
||||||
geom = None
|
|
||||||
elif gs.geom:
|
|
||||||
try:
|
|
||||||
geom = json.loads(gs.geom.transform(4326, clone=True).geojson)
|
|
||||||
except Exception:
|
|
||||||
geom = None
|
|
||||||
|
|
||||||
if geom:
|
|
||||||
assets_features.append({
|
|
||||||
"type": "Feature",
|
|
||||||
"id": f"gs_{gs.id}",
|
|
||||||
"geometry": geom,
|
|
||||||
"properties": {
|
|
||||||
"id": gs.id,
|
|
||||||
"code": gs.code,
|
|
||||||
"name": gs.get_name(),
|
|
||||||
"model": gs.model.get_name() if gs.model else '',
|
|
||||||
"asset_type": "naturegreensurface",
|
|
||||||
"area": f"{gs.surface_area:.1f} m²" if gs.surface_area else '',
|
|
||||||
"color": gs.model.color if (gs.model and gs.model.color) else '#4caf50',
|
|
||||||
"status": gs.get_status_display(),
|
|
||||||
"lon": gs.lon,
|
|
||||||
"lat": gs.lat,
|
|
||||||
"detail_url": reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturegreensurface', 'asset_id': gs.id}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for tree in trees:
|
|
||||||
geom = None
|
|
||||||
if tree.geojson:
|
|
||||||
try:
|
|
||||||
geom = json.loads(tree.geojson)
|
|
||||||
except Exception:
|
|
||||||
geom = None
|
|
||||||
elif tree.lon is not None and tree.lat is not None:
|
|
||||||
geom = {"type": "Point", "coordinates": [tree.lon, tree.lat]}
|
|
||||||
|
|
||||||
if geom:
|
|
||||||
assets_features.append({
|
|
||||||
"type": "Feature",
|
|
||||||
"id": f"tree_{tree.id}",
|
|
||||||
"geometry": geom,
|
|
||||||
"properties": {
|
|
||||||
"id": tree.id,
|
|
||||||
"code": tree.code,
|
|
||||||
"name": tree.get_name() or (tree.model.get_name() if tree.model else ''),
|
|
||||||
"model": tree.model.get_name() if tree.model else '',
|
|
||||||
"scientific_name": tree.model.scientific_name if (tree.model and tree.model.scientific_name) else '',
|
|
||||||
"asset_type": "naturetree",
|
|
||||||
"color": "#2e7d32",
|
|
||||||
"vitality": tree.get_vitality_display() if tree.vitality else '',
|
|
||||||
"stage": tree.get_development_stage_display() if tree.development_stage else '',
|
|
||||||
"height": f"{tree.height} m" if tree.height else '',
|
|
||||||
"status": tree.get_status_display(),
|
|
||||||
"is_remarkable": tree.is_remarkable,
|
|
||||||
"lon": tree.lon,
|
|
||||||
"lat": tree.lat,
|
|
||||||
"detail_url": reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturetree', 'asset_id': tree.id}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for shrub in shrubs:
|
|
||||||
geom = None
|
|
||||||
if shrub.geojson:
|
|
||||||
try:
|
|
||||||
geom = json.loads(shrub.geojson)
|
|
||||||
except Exception:
|
|
||||||
geom = None
|
|
||||||
elif shrub.lon is not None and shrub.lat is not None:
|
|
||||||
geom = {"type": "Point", "coordinates": [shrub.lon, shrub.lat]}
|
|
||||||
|
|
||||||
if geom:
|
|
||||||
assets_features.append({
|
|
||||||
"type": "Feature",
|
|
||||||
"id": f"shrub_{shrub.id}",
|
|
||||||
"geometry": geom,
|
|
||||||
"properties": {
|
|
||||||
"id": shrub.id,
|
|
||||||
"code": shrub.code,
|
|
||||||
"model": shrub.model.get_name() if shrub.model else '',
|
|
||||||
"asset_type": "natureshrub",
|
|
||||||
"color": "#81c784",
|
|
||||||
"height": f"{shrub.height} m" if shrub.height else '',
|
|
||||||
"status": shrub.get_status_display(),
|
|
||||||
"lon": shrub.lon,
|
|
||||||
"lat": shrub.lat,
|
|
||||||
"detail_url": reverse('assets:nature_assets_detail', kwargs={'asset_model': 'natureshrub', 'asset_id': shrub.id}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for bench in benches:
|
|
||||||
geom = None
|
|
||||||
if bench.geojson:
|
|
||||||
try:
|
|
||||||
geom = json.loads(bench.geojson)
|
|
||||||
except Exception:
|
|
||||||
geom = None
|
|
||||||
elif bench.lon is not None and bench.lat is not None:
|
|
||||||
geom = {"type": "Point", "coordinates": [bench.lon, bench.lat]}
|
|
||||||
|
|
||||||
if geom:
|
|
||||||
assets_features.append({
|
|
||||||
"type": "Feature",
|
|
||||||
"id": f"bench_{bench.id}",
|
|
||||||
"geometry": geom,
|
|
||||||
"properties": {
|
|
||||||
"id": bench.id,
|
|
||||||
"code": bench.code,
|
|
||||||
"model": bench.model.get_name() if bench.model else '',
|
|
||||||
"asset_type": "naturebench",
|
|
||||||
"color": "#8d6e63",
|
|
||||||
"material": bench.material or '',
|
|
||||||
"status": bench.get_status_display(),
|
|
||||||
"lon": bench.lon,
|
|
||||||
"lat": bench.lat,
|
|
||||||
"detail_url": reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturebench', 'asset_id': bench.id}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
assets_geojson = json.dumps({"type": "FeatureCollection", "features": assets_features})
|
|
||||||
|
|
||||||
# Location geometry for map
|
|
||||||
section_geojson = section.geojson
|
|
||||||
if not section_geojson and section.geom:
|
|
||||||
try:
|
|
||||||
section_geojson = section.geom.transform(4326, clone=True).geojson
|
|
||||||
except Exception:
|
|
||||||
section_geojson = None
|
|
||||||
|
|
||||||
feature_lon = section.lon
|
|
||||||
feature_lat = section.lat
|
|
||||||
if (feature_lon is None or feature_lat is None) and section.geom:
|
|
||||||
try:
|
|
||||||
pt = section.geom.transform(4326, clone=True).centroid
|
|
||||||
feature_lon = pt.x
|
|
||||||
feature_lat = pt.y
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'section': section,
|
'section': section,
|
||||||
'trees': trees,
|
|
||||||
'green_surfaces': green_surfaces,
|
|
||||||
'shrubs': shrubs,
|
|
||||||
'benches': benches,
|
|
||||||
'trees_count': trees_count,
|
|
||||||
'trees_active_count': trees_active_count,
|
|
||||||
'trees_remarkable_count': trees_remarkable_count,
|
|
||||||
'green_surfaces_count': green_surfaces_count,
|
|
||||||
'total_green_surface': total_green_surface,
|
|
||||||
'surface_massif_arbore': surface_massif_arbore,
|
|
||||||
'massif_arbore_count': massif_arbore_count,
|
|
||||||
'surface_pre_fleuri': surface_pre_fleuri,
|
|
||||||
'pre_fleuri_count': pre_fleuri_count,
|
|
||||||
'surfaces_by_type': surfaces_by_type,
|
|
||||||
'shrubs_count': shrubs_count,
|
|
||||||
'benches_count': benches_count,
|
|
||||||
'total_assets_count': total_assets_count,
|
|
||||||
'interventions': interventions,
|
'interventions': interventions,
|
||||||
'documents': documents,
|
|
||||||
'assets_geojson': assets_geojson,
|
|
||||||
'section_geojson': section_geojson,
|
|
||||||
'feature_lon': feature_lon,
|
|
||||||
'feature_lat': feature_lat,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "assets/nature_locations_detail.html", context)
|
return render(request, "assets/nature_locations_detail.html", context)
|
||||||
|
|
@ -378,30 +159,10 @@ def nature_locations_geojson(request):
|
||||||
filter_geom = None
|
filter_geom = None
|
||||||
|
|
||||||
|
|
||||||
from assets.permissions import get_user_accessible_municipalities
|
|
||||||
user = request.user
|
|
||||||
user_config = getattr(user, 'config', None)
|
|
||||||
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
|
||||||
is_external = (user_config is None) or (not user_config.is_intern)
|
|
||||||
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
|
||||||
|
|
||||||
municipalities = ensure_list(data.get('municipality')) if request.method == "POST" else request.GET.getlist('municipality')
|
|
||||||
|
|
||||||
include_naturelocation = location_type in ('', 'naturelocation')
|
include_naturelocation = location_type in ('', 'naturelocation')
|
||||||
|
|
||||||
nature_locations = NatureLocation.objects.all() if include_naturelocation else NatureLocation.objects.none()
|
nature_locations = NatureLocation.objects.all() if include_naturelocation else NatureLocation.objects.none()
|
||||||
|
|
||||||
if not is_admin and must_limit:
|
|
||||||
accessible_munis = get_user_accessible_municipalities(user)
|
|
||||||
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
|
|
||||||
if municipalities:
|
|
||||||
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
|
|
||||||
nature_locations = nature_locations.filter(municipality_id__in=valid_munis)
|
|
||||||
else:
|
|
||||||
nature_locations = nature_locations.filter(municipality_id__in=accessible_muni_ids)
|
|
||||||
elif municipalities:
|
|
||||||
nature_locations = nature_locations.filter(municipality_id__in=municipalities)
|
|
||||||
|
|
||||||
if highway:
|
if highway:
|
||||||
nature_locations = nature_locations.filter(is_highway=highway)
|
nature_locations = nature_locations.filter(is_highway=highway)
|
||||||
|
|
||||||
|
|
@ -497,7 +258,7 @@ def ensure_list(value):
|
||||||
return [value]
|
return [value]
|
||||||
|
|
||||||
|
|
||||||
MAX_ASSET_FEATURES = 10000 # limite imposée
|
MAX_ASSET_FEATURES = 5000 # limite imposée
|
||||||
|
|
||||||
@check_thematic_access('nature')
|
@check_thematic_access('nature')
|
||||||
def nature_assets_geojson(request):
|
def nature_assets_geojson(request):
|
||||||
|
|
@ -516,7 +277,6 @@ def nature_assets_geojson(request):
|
||||||
models = ensure_list(data.get('model'))
|
models = ensure_list(data.get('model'))
|
||||||
highway = data.get('highway') == "1"
|
highway = data.get('highway') == "1"
|
||||||
location_type = data.get('location_type', '')
|
location_type = data.get('location_type', '')
|
||||||
management_types = ensure_list(data.get('management_type'))
|
|
||||||
|
|
||||||
# Récupération et parsing de la géométrie
|
# Récupération et parsing de la géométrie
|
||||||
filter_geom_raw = data.get("filter_geom")
|
filter_geom_raw = data.get("filter_geom")
|
||||||
|
|
@ -537,12 +297,11 @@ def nature_assets_geojson(request):
|
||||||
models = request.GET.getlist('model', [])
|
models = request.GET.getlist('model', [])
|
||||||
highway = request.GET.get('highway') == "1"
|
highway = request.GET.get('highway') == "1"
|
||||||
location_type = request.GET.get('location_type', '')
|
location_type = request.GET.get('location_type', '')
|
||||||
management_types = request.GET.getlist('management_type', [])
|
|
||||||
filter_geom = None
|
filter_geom = None
|
||||||
|
|
||||||
|
|
||||||
_search_q = (data.get('q') or '').strip() if request.method == 'POST' else ''
|
_search_q = (data.get('q') or '').strip() if request.method == 'POST' else ''
|
||||||
if not request.GET.get('unified') and not sections_ids and not categories and not models and not filter_geom and not _search_q and not highway and not location_type and not management_types: # return empty geojson if no param with section_id
|
if not request.GET.get('unified') and not sections_ids and not categories and not models and not filter_geom and not _search_q and not highway and not location_type: # return empty geojson if no param with section_id
|
||||||
geojson_data = {
|
geojson_data = {
|
||||||
"type": "FeatureCollection",
|
"type": "FeatureCollection",
|
||||||
"features": [],
|
"features": [],
|
||||||
|
|
@ -612,13 +371,6 @@ def nature_assets_geojson(request):
|
||||||
"thematic": "nature",
|
"thematic": "nature",
|
||||||
"is_location": False,
|
"is_location": False,
|
||||||
"structure_type": obj.get('structure_type', ''),
|
"structure_type": obj.get('structure_type', ''),
|
||||||
"managing_authority": obj.get('managing_authority') or '',
|
|
||||||
"management_type": obj.get('management_type') or '',
|
|
||||||
"is_remarkable": obj.get('is_remarkable', False),
|
|
||||||
"scientific_name": obj.get('scientific_name') or '',
|
|
||||||
"vernacular_name": obj.get('vernacular_name') or '',
|
|
||||||
"is_dead": bool(obj.get('is_dead') or obj.get('vitality') == 'dead' or obj.get('status') in ('dead', 'archived')),
|
|
||||||
"vitality": obj.get('vitality') or '',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -645,35 +397,6 @@ def nature_assets_geojson(request):
|
||||||
NatureBench.objects.all()
|
NatureBench.objects.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
from assets.permissions import get_user_accessible_municipalities
|
|
||||||
user = request.user
|
|
||||||
user_config = getattr(user, 'config', None)
|
|
||||||
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
|
||||||
is_external = (user_config is None) or (not user_config.is_intern)
|
|
||||||
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
|
||||||
|
|
||||||
municipalities = ensure_list(data.get('municipality')) if request.method == "POST" else request.GET.getlist('municipality')
|
|
||||||
|
|
||||||
if not is_admin and must_limit:
|
|
||||||
accessible_munis = get_user_accessible_municipalities(user)
|
|
||||||
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
|
|
||||||
if municipalities:
|
|
||||||
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
|
|
||||||
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=valid_munis)
|
|
||||||
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=valid_munis)
|
|
||||||
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=valid_munis)
|
|
||||||
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=valid_munis)
|
|
||||||
else:
|
|
||||||
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=accessible_muni_ids)
|
|
||||||
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=accessible_muni_ids)
|
|
||||||
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=accessible_muni_ids)
|
|
||||||
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=accessible_muni_ids)
|
|
||||||
elif municipalities:
|
|
||||||
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=municipalities)
|
|
||||||
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=municipalities)
|
|
||||||
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=municipalities)
|
|
||||||
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=municipalities)
|
|
||||||
|
|
||||||
include_naturelocation = location_type in ('', 'naturelocation')
|
include_naturelocation = location_type in ('', 'naturelocation')
|
||||||
|
|
||||||
if not include_naturelocation:
|
if not include_naturelocation:
|
||||||
|
|
@ -682,6 +405,7 @@ def nature_assets_geojson(request):
|
||||||
natureshrub_qs = natureshrub_qs.none()
|
natureshrub_qs = natureshrub_qs.none()
|
||||||
naturebench_qs = naturebench_qs.none()
|
naturebench_qs = naturebench_qs.none()
|
||||||
|
|
||||||
|
|
||||||
if sections_ids:
|
if sections_ids:
|
||||||
naturegreensurface_qs = naturegreensurface_qs.filter(location__id__in=sections_ids)
|
naturegreensurface_qs = naturegreensurface_qs.filter(location__id__in=sections_ids)
|
||||||
naturetree_qs = naturetree_qs.filter(location__id__in=sections_ids)
|
naturetree_qs = naturetree_qs.filter(location__id__in=sections_ids)
|
||||||
|
|
@ -698,45 +422,6 @@ def nature_assets_geojson(request):
|
||||||
naturetree_qs = naturetree_qs.filter(model__id__in=models)
|
naturetree_qs = naturetree_qs.filter(model__id__in=models)
|
||||||
natureshrub_qs = natureshrub_qs.filter(model__id__in=models)
|
natureshrub_qs = natureshrub_qs.filter(model__id__in=models)
|
||||||
naturebench_qs = naturebench_qs.filter(model__id__in=models)
|
naturebench_qs = naturebench_qs.filter(model__id__in=models)
|
||||||
|
|
||||||
if management_types:
|
|
||||||
q_mgmt = Q()
|
|
||||||
for val in management_types:
|
|
||||||
val_str = str(val).strip()
|
|
||||||
if val_str == 'municipal':
|
|
||||||
q_mgmt |= Q(management_type='municipal')
|
|
||||||
elif val_str == 'regional':
|
|
||||||
q_mgmt |= Q(management_type='regional')
|
|
||||||
elif val_str == 'regional_bruxelles':
|
|
||||||
q_mgmt |= Q(management_type='regional') & (
|
|
||||||
Q(location__municipality__niscode__startswith='21') |
|
|
||||||
Q(location__municipality__name_fr__icontains='Bruxelles') |
|
|
||||||
Q(location__municipality__name_fr__icontains='Watermael') |
|
|
||||||
Q(location__municipality__name_fr__icontains='Berchem')
|
|
||||||
)
|
|
||||||
elif val_str == 'regional_wallonie':
|
|
||||||
q_mgmt |= Q(management_type='regional') & (
|
|
||||||
Q(location__municipality__niscode__startswith=('25', '5', '6', '8', '9')) |
|
|
||||||
Q(location__municipality__name_fr__icontains='Court-Saint')
|
|
||||||
)
|
|
||||||
elif val_str == 'regional_flandre':
|
|
||||||
q_mgmt |= Q(management_type='regional') & (
|
|
||||||
Q(location__municipality__niscode__startswith=('1', '3', '4', '7', '23', '24'))
|
|
||||||
)
|
|
||||||
elif val_str.startswith('municipal_'):
|
|
||||||
m_id = val_str.split('_', 1)[1]
|
|
||||||
if m_id.isdigit():
|
|
||||||
q_mgmt |= Q(management_type='municipal', location__municipality_id=int(m_id))
|
|
||||||
elif val_str.startswith('authority:'):
|
|
||||||
auth_name = val_str.split(':', 1)[1]
|
|
||||||
q_mgmt |= Q(managing_authority__iexact=auth_name)
|
|
||||||
else:
|
|
||||||
q_mgmt |= Q(managing_authority__icontains=val_str) | Q(management_type=val_str)
|
|
||||||
|
|
||||||
naturetree_qs = naturetree_qs.filter(q_mgmt)
|
|
||||||
naturegreensurface_qs = naturegreensurface_qs.none()
|
|
||||||
natureshrub_qs = natureshrub_qs.none()
|
|
||||||
naturebench_qs = naturebench_qs.none()
|
|
||||||
if categories:
|
if categories:
|
||||||
naturegreensurface_qs = naturegreensurface_qs.filter(category__id__in=categories)
|
naturegreensurface_qs = naturegreensurface_qs.filter(category__id__in=categories)
|
||||||
naturetree_qs = naturetree_qs.filter(category__id__in=categories)
|
naturetree_qs = naturetree_qs.filter(category__id__in=categories)
|
||||||
|
|
@ -812,10 +497,7 @@ def nature_assets_geojson(request):
|
||||||
.values( # on limite strictement les colonnes
|
.values( # on limite strictement les colonnes
|
||||||
'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat',
|
'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat',
|
||||||
'category_name', 'model_name', 'color', 'geojson',
|
'category_name', 'model_name', 'color', 'geojson',
|
||||||
'category_id', 'model_id', 'location_lon', 'location_lat',
|
'category_id', 'model_id', 'location_lon', 'location_lat'
|
||||||
'managing_authority', 'management_type', 'is_remarkable',
|
|
||||||
'scientific_name', 'vernacular_name',
|
|
||||||
'is_dead', 'vitality'
|
|
||||||
)
|
)
|
||||||
.order_by('code')
|
.order_by('code')
|
||||||
)[:MAX_ASSET_FEATURES + 1]
|
)[:MAX_ASSET_FEATURES + 1]
|
||||||
|
|
@ -887,24 +569,12 @@ def nature_locations_autocomplete(request):
|
||||||
if len(raw_query) < 2:
|
if len(raw_query) < 2:
|
||||||
return JsonResponse([], safe=False)
|
return JsonResponse([], safe=False)
|
||||||
|
|
||||||
user = request.user
|
|
||||||
user_config = getattr(user, 'config', None)
|
|
||||||
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
|
||||||
is_external = (user_config is None) or (not user_config.is_intern)
|
|
||||||
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
|
||||||
|
|
||||||
# Normalise la requête (équivalent de unaccent+lower côté Python)
|
# Normalise la requête (équivalent de unaccent+lower côté Python)
|
||||||
qn = strip_accents_lower(raw_query)
|
qn = strip_accents_lower(raw_query)
|
||||||
|
|
||||||
# Prépare des colonnes "recherchables" unaccentées + minuscules
|
# Prépare des colonnes "recherchables" unaccentées + minuscules
|
||||||
qs = NatureLocation.objects.all()
|
|
||||||
if not is_admin and must_limit:
|
|
||||||
from assets.permissions import get_user_accessible_municipalities
|
|
||||||
accessible_munis = get_user_accessible_municipalities(user)
|
|
||||||
qs = qs.filter(municipality__in=accessible_munis)
|
|
||||||
|
|
||||||
qs = (
|
qs = (
|
||||||
qs
|
NatureLocation.objects
|
||||||
.annotate(
|
.annotate(
|
||||||
code_u=Lower(Unaccent('code')),
|
code_u=Lower(Unaccent('code')),
|
||||||
name_fr_u=Lower(Unaccent('name_fr')),
|
name_fr_u=Lower(Unaccent('name_fr')),
|
||||||
|
|
@ -969,25 +639,13 @@ def nature_assets_autocomplete(request):
|
||||||
location_filter = {'location_id': location_id}
|
location_filter = {'location_id': location_id}
|
||||||
|
|
||||||
|
|
||||||
user = request.user
|
|
||||||
user_config = getattr(user, 'config', None)
|
|
||||||
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
|
||||||
is_external = (user_config is None) or (not user_config.is_intern)
|
|
||||||
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
|
||||||
|
|
||||||
def base_qs(model, extra_filter=None):
|
def base_qs(model, extra_filter=None):
|
||||||
ann = {
|
ann = {
|
||||||
'code_u': Lower(Unaccent('code')),
|
'code_u': Lower(Unaccent('code')),
|
||||||
'name_fr_u': Lower(Unaccent('name_fr')),
|
'name_fr_u': Lower(Unaccent('name_fr')),
|
||||||
'name_nl_u': Lower(Unaccent('name_nl')),
|
'name_nl_u': Lower(Unaccent('name_nl')),
|
||||||
}
|
}
|
||||||
q = model.objects.all()
|
q = model.objects.annotate(**ann)
|
||||||
if not is_admin and must_limit:
|
|
||||||
from assets.permissions import get_user_accessible_municipalities
|
|
||||||
accessible_munis = get_user_accessible_municipalities(user)
|
|
||||||
q = q.filter(location__municipality__in=accessible_munis)
|
|
||||||
|
|
||||||
q = q.annotate(**ann)
|
|
||||||
|
|
||||||
# Si un terme de recherche est fourni, filtrer dessus
|
# Si un terme de recherche est fourni, filtrer dessus
|
||||||
if has_search_term:
|
if has_search_term:
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ def structures_index(request):
|
||||||
selected_locations_ids = draft_data.get('locations', [])
|
selected_locations_ids = draft_data.get('locations', [])
|
||||||
|
|
||||||
|
|
||||||
user_config, _created = UserConfig.objects.get_or_create(user=request.user)
|
user_config, _ = UserConfig.objects.get_or_create(user=request.user)
|
||||||
thematic_choices_qs = (
|
thematic_choices_qs = (
|
||||||
UserThematics.objects
|
UserThematics.objects
|
||||||
.filter(user_config=user_config, can_view_assets=True)
|
.filter(user_config=user_config, can_view_assets=True)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ from django.utils.translation import get_language, gettext as _
|
||||||
from django.utils.timezone import make_aware, get_current_timezone
|
from django.utils.timezone import make_aware, get_current_timezone
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
@ -3745,77 +3744,6 @@ def update_staged_pole_position(request, intersection_id, pole_id):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def find_db_lantern_model(model_name):
|
|
||||||
"""
|
|
||||||
Finds the most suitable TrafficLightLanternModel from the database based on model_name.
|
|
||||||
Gracefully handles variants such as '2V200 cyclo-piéto + HP', '2V200 cyclo-piéton + HP',
|
|
||||||
'2V200 piéton+HP', tram, cyclist models, etc.
|
|
||||||
"""
|
|
||||||
if not model_name:
|
|
||||||
return TrafficLightLanternModel.objects.first()
|
|
||||||
m = TrafficLightLanternModel.objects.filter(name_fr__iexact=model_name).first()
|
|
||||||
if m:
|
|
||||||
return m
|
|
||||||
m_nl = TrafficLightLanternModel.objects.filter(name_nl__iexact=model_name).first()
|
|
||||||
if m_nl:
|
|
||||||
return m_nl
|
|
||||||
model_lower = str(model_name).lower()
|
|
||||||
if "cyclo" in model_lower:
|
|
||||||
qs = TrafficLightLanternModel.objects.filter(
|
|
||||||
Q(name_fr__icontains="cyclo") | Q(name_nl__icontains="cyclo") |
|
|
||||||
Q(name_fr__icontains="fietsers- en voetgangers") | Q(name_nl__icontains="fietsers- en voetgangers")
|
|
||||||
)
|
|
||||||
if "hp" in model_lower or "haut-parleur" in model_lower:
|
|
||||||
m = qs.filter(Q(name_fr__icontains="hp") | Q(name_fr__icontains="haut-parleur") | Q(name_nl__icontains="hp")).first()
|
|
||||||
else:
|
|
||||||
m = qs.exclude(Q(name_fr__icontains="hp") | Q(name_fr__icontains="haut-parleur") | Q(name_nl__icontains="hp")).first()
|
|
||||||
if m:
|
|
||||||
return m
|
|
||||||
if qs.exists():
|
|
||||||
return qs.first()
|
|
||||||
elif any(w in model_lower for w in ["piét", "piet", "voetganger"]):
|
|
||||||
qs = TrafficLightLanternModel.objects.filter(
|
|
||||||
Q(name_fr__icontains="piét") | Q(name_fr__icontains="piet") | Q(name_nl__icontains="voetganger")
|
|
||||||
).exclude(Q(name_fr__icontains="cyclo") | Q(name_nl__icontains="cyclo"))
|
|
||||||
if "hp" in model_lower or "haut-parleur" in model_lower:
|
|
||||||
m = qs.filter(Q(name_fr__icontains="hp") | Q(name_fr__icontains="haut-parleur") | Q(name_nl__icontains="hp")).first()
|
|
||||||
else:
|
|
||||||
m = qs.exclude(Q(name_fr__icontains="hp") | Q(name_fr__icontains="haut-parleur") | Q(name_nl__icontains="hp")).first()
|
|
||||||
if m:
|
|
||||||
return m
|
|
||||||
elif "tram" in model_lower:
|
|
||||||
if "300" in model_lower:
|
|
||||||
m = TrafficLightLanternModel.objects.filter(name_fr__icontains="tram").filter(name_fr__icontains="300").first()
|
|
||||||
if m:
|
|
||||||
return m
|
|
||||||
m = TrafficLightLanternModel.objects.filter(name_fr__icontains="tram").first()
|
|
||||||
if m:
|
|
||||||
return m
|
|
||||||
elif any(w in model_lower for w in ["cycliste", "cycl", "fiets"]):
|
|
||||||
m = TrafficLightLanternModel.objects.filter(
|
|
||||||
Q(name_fr__icontains="cycl") | Q(name_nl__icontains="fiets")
|
|
||||||
).exclude(Q(name_fr__icontains="piét") | Q(name_fr__icontains="piet") | Q(name_nl__icontains="voetganger")).first()
|
|
||||||
if m:
|
|
||||||
return m
|
|
||||||
return TrafficLightLanternModel.objects.first()
|
|
||||||
|
|
||||||
|
|
||||||
def is_stib_detector(detector_obj, code=""):
|
|
||||||
"""
|
|
||||||
Checks if a detector is a STIB/tram detection loop (e.g. dT1, dT2, DT1),
|
|
||||||
which is provided/managed by STIB and not present in cross plans.
|
|
||||||
"""
|
|
||||||
name = (getattr(detector_obj, "name", None) or getattr(detector_obj, "name_fr", None) or "").strip()
|
|
||||||
code_str = (code or getattr(detector_obj, "code", "") or "").strip()
|
|
||||||
if re.match(r'^dt[\s_\d]*$', name, re.IGNORECASE) or name.lower().startswith("dt"):
|
|
||||||
return True
|
|
||||||
if re.search(r'\bdt\d+\b', name, re.IGNORECASE) or "stib" in name.lower():
|
|
||||||
return True
|
|
||||||
if re.search(r'(?:^|_)dt\d*(?:_|$)', code_str, re.IGNORECASE) or "stib" in code_str.lower():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@check_thematic_access('trafficlights', edit_permission_required=True)
|
@check_thematic_access('trafficlights', edit_permission_required=True)
|
||||||
def import_cross_plan(request, intersection_id):
|
def import_cross_plan(request, intersection_id):
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
|
@ -4401,15 +4329,6 @@ def import_cross_plan(request, intersection_id):
|
||||||
geom_modified = True
|
geom_modified = True
|
||||||
details_diff.append(_("Nouveau tracé déduit des poteaux"))
|
details_diff.append(_("Nouveau tracé déduit des poteaux"))
|
||||||
|
|
||||||
db_coords = []
|
|
||||||
if has_geom:
|
|
||||||
try:
|
|
||||||
gw = ec.geom.clone()
|
|
||||||
gw.transform(4326)
|
|
||||||
db_coords = [[round(c[0], 6), round(c[1], 6)] for c in gw.coords]
|
|
||||||
except Exception:
|
|
||||||
db_coords = []
|
|
||||||
|
|
||||||
status = 'modified' if (is_diff or geom_modified) else 'identical'
|
status = 'modified' if (is_diff or geom_modified) else 'identical'
|
||||||
compared_cables.append({
|
compared_cables.append({
|
||||||
'code': code,
|
'code': code,
|
||||||
|
|
@ -4423,7 +4342,6 @@ def import_cross_plan(request, intersection_id):
|
||||||
'has_db_geom': has_geom,
|
'has_db_geom': has_geom,
|
||||||
'geom_modified': geom_modified,
|
'geom_modified': geom_modified,
|
||||||
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
||||||
'db_coords': db_coords,
|
|
||||||
})
|
})
|
||||||
existing_cables.pop(code)
|
existing_cables.pop(code)
|
||||||
else:
|
else:
|
||||||
|
|
@ -4442,18 +4360,9 @@ def import_cross_plan(request, intersection_id):
|
||||||
'has_db_geom': False,
|
'has_db_geom': False,
|
||||||
'geom_modified': False,
|
'geom_modified': False,
|
||||||
'db_geom_length': None,
|
'db_geom_length': None,
|
||||||
'db_coords': [],
|
|
||||||
})
|
})
|
||||||
for code, ec in existing_cables.items():
|
for code, ec in existing_cables.items():
|
||||||
has_geom = ec.geom is not None
|
has_geom = ec.geom is not None
|
||||||
db_coords = []
|
|
||||||
if has_geom:
|
|
||||||
try:
|
|
||||||
gw = ec.geom.clone()
|
|
||||||
gw.transform(4326)
|
|
||||||
db_coords = [[round(c[0], 6), round(c[1], 6)] for c in gw.coords]
|
|
||||||
except Exception:
|
|
||||||
db_coords = []
|
|
||||||
compared_cables.append({
|
compared_cables.append({
|
||||||
'code': code,
|
'code': code,
|
||||||
'name': ec.name_fr or code,
|
'name': ec.name_fr or code,
|
||||||
|
|
@ -4465,7 +4374,6 @@ def import_cross_plan(request, intersection_id):
|
||||||
'has_db_geom': has_geom,
|
'has_db_geom': has_geom,
|
||||||
'geom_modified': False,
|
'geom_modified': False,
|
||||||
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
||||||
'db_coords': db_coords,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── 3. LANTERNS COMPARISON ──
|
# ── 3. LANTERNS COMPARISON ──
|
||||||
|
|
@ -4505,7 +4413,7 @@ def import_cross_plan(request, intersection_id):
|
||||||
for dl, el in matched_pairs:
|
for dl, el in matched_pairs:
|
||||||
code = dl["code"]
|
code = dl["code"]
|
||||||
model_name = dl["model_name"]
|
model_name = dl["model_name"]
|
||||||
db_model = find_db_lantern_model(model_name)
|
db_model = TrafficLightLanternModel.objects.filter(name_fr__iexact=model_name).first() or TrafficLightLanternModel.objects.first()
|
||||||
|
|
||||||
is_rename = (el.code != code)
|
is_rename = (el.code != code)
|
||||||
is_model_diff = el.model_id != (db_model.id if db_model else None)
|
is_model_diff = el.model_id != (db_model.id if db_model else None)
|
||||||
|
|
@ -4515,7 +4423,7 @@ def import_cross_plan(request, intersection_id):
|
||||||
if is_rename:
|
if is_rename:
|
||||||
details.append(f"Code : {el.code} → {code}")
|
details.append(f"Code : {el.code} → {code}")
|
||||||
if is_model_diff:
|
if is_model_diff:
|
||||||
details.append(f"Modèle : {el.model.name_fr if el.model else 'Aucun'} → {db_model.name_fr if db_model else model_name}")
|
details.append(f"Modèle : {el.model.name_fr if el.model else 'Aucun'} → {model_name}")
|
||||||
|
|
||||||
if is_model_diff and el.name_fr and el.model and el.name_fr.strip().lower() == el.model.name_fr.strip().lower():
|
if is_model_diff and el.name_fr and el.model and el.name_fr.strip().lower() == el.model.name_fr.strip().lower():
|
||||||
display_name = dl['name']
|
display_name = dl['name']
|
||||||
|
|
@ -4526,7 +4434,7 @@ def import_cross_plan(request, intersection_id):
|
||||||
'code': code,
|
'code': code,
|
||||||
'name': display_name,
|
'name': display_name,
|
||||||
'status': status,
|
'status': status,
|
||||||
'deduced_model_name': db_model.name_fr if db_model else model_name,
|
'deduced_model_name': model_name,
|
||||||
'db_model_name': el.model.name_fr if el.model else '',
|
'db_model_name': el.model.name_fr if el.model else '',
|
||||||
'db_id': el.id,
|
'db_id': el.id,
|
||||||
'pole_code': dl['pole_code'],
|
'pole_code': dl['pole_code'],
|
||||||
|
|
@ -4624,9 +4532,6 @@ def import_cross_plan(request, intersection_id):
|
||||||
'details_diff': []
|
'details_diff': []
|
||||||
})
|
})
|
||||||
for code, ed in existing_detectors.items():
|
for code, ed in existing_detectors.items():
|
||||||
is_stib = is_stib_detector(ed, code)
|
|
||||||
action_default = 'keep' if is_stib else 'archive'
|
|
||||||
diff_text = _("Détection STIB / Tram (fournie par la STIB, conservée par défaut)") if is_stib else _("Absent du fichier Excel (présent en DB)")
|
|
||||||
compared_detectors.append({
|
compared_detectors.append({
|
||||||
'code': code,
|
'code': code,
|
||||||
'name': ed.name or code,
|
'name': ed.name or code,
|
||||||
|
|
@ -4635,9 +4540,7 @@ def import_cross_plan(request, intersection_id):
|
||||||
'db_model_name': ed.model.name_fr if ed.model else '',
|
'db_model_name': ed.model.name_fr if ed.model else '',
|
||||||
'db_id': ed.id,
|
'db_id': ed.id,
|
||||||
'pole_code': ed.pole.code if ed.pole else '',
|
'pole_code': ed.pole.code if ed.pole else '',
|
||||||
'details_diff': [diff_text],
|
'details_diff': [_("Absent du fichier Excel (présent en DB)")]
|
||||||
'action_default': action_default,
|
|
||||||
'is_stib': is_stib,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── 5. OTHER ASSOCIATED ASSETS (NOT IN CROSS PLAN) ──
|
# ── 5. OTHER ASSOCIATED ASSETS (NOT IN CROSS PLAN) ──
|
||||||
|
|
@ -4887,18 +4790,12 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import ProtectedError
|
import json
|
||||||
from assets.models.trafficlights import (
|
from assets.models.trafficlights import (
|
||||||
TrafficLightIntersection, TrafficLightPole, TrafficLightLantern,
|
TrafficLightIntersection, TrafficLightPole, TrafficLightLantern,
|
||||||
TrafficLightDetector, TrafficLightCable, TrafficLightPoleModel,
|
TrafficLightDetector, TrafficLightCable, TrafficLightPoleModel,
|
||||||
TrafficLightLanternModel, TrafficLightDetectorModel, TrafficLightCableModel,
|
TrafficLightLanternModel, TrafficLightDetectorModel, TrafficLightCableModel
|
||||||
TrafficLightController, TrafficLightControllerHardware, TrafficLightNetworkHardware,
|
|
||||||
TrafficLightVoltageHardware, TrafficLightElectricalCabinet, TrafficLightAccessory,
|
|
||||||
TrafficLightRadar, TrafficLightProgramming
|
|
||||||
)
|
)
|
||||||
from assets.models.sign import SignPanel
|
|
||||||
from assets.models.its import ITSGeoAsset
|
|
||||||
from assets.models.publiclighting import PublicLightingLightFixture
|
|
||||||
|
|
||||||
intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id)
|
intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id)
|
||||||
filename = request.POST.get('cross_plan_filename')
|
filename = request.POST.get('cross_plan_filename')
|
||||||
|
|
@ -4935,7 +4832,7 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
return m
|
return m
|
||||||
|
|
||||||
def resolve_lantern_model(name_fr):
|
def resolve_lantern_model(name_fr):
|
||||||
m = find_db_lantern_model(name_fr)
|
m = TrafficLightLanternModel.objects.filter(name_fr__iexact=name_fr).first()
|
||||||
if not m:
|
if not m:
|
||||||
m = TrafficLightLanternModel.objects.first()
|
m = TrafficLightLanternModel.objects.first()
|
||||||
if not m:
|
if not m:
|
||||||
|
|
@ -5432,18 +5329,6 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
PublicLightingLightFixture.objects.filter(traffic_light_pole=existing).update(traffic_light_pole=new_pole)
|
PublicLightingLightFixture.objects.filter(traffic_light_pole=existing).update(traffic_light_pole=new_pole)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
elif action == "archive" and existing:
|
|
||||||
existing.status = 'archived'
|
|
||||||
existing.updated_by = request.user
|
|
||||||
existing.save()
|
|
||||||
elif action == "delete" and existing:
|
|
||||||
try:
|
|
||||||
existing.delete()
|
|
||||||
except ProtectedError:
|
|
||||||
existing.status = 'archived'
|
|
||||||
existing.updated_by = request.user
|
|
||||||
existing.save()
|
|
||||||
messages.warning(request, f"Le poteau {existing.code} n'a pas pu être supprimé car d'autres éléments y font référence ; il a été archivé à la place.")
|
|
||||||
else:
|
else:
|
||||||
# 'keep' (Ne rien faire)
|
# 'keep' (Ne rien faire)
|
||||||
if existing:
|
if existing:
|
||||||
|
|
@ -5462,17 +5347,6 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
TrafficLightPole.objects.filter(intersection=intersection, code=code, status='active').update(
|
TrafficLightPole.objects.filter(intersection=intersection, code=code, status='active').update(
|
||||||
status='archived', updated_by=request.user
|
status='archived', updated_by=request.user
|
||||||
)
|
)
|
||||||
elif action == "delete":
|
|
||||||
db_id = request.POST.get(f"db_id_pole_{code}")
|
|
||||||
qs = TrafficLightPole.objects.filter(pk=db_id) if db_id else TrafficLightPole.objects.filter(intersection=intersection, code=code)
|
|
||||||
for pole_to_del in qs:
|
|
||||||
try:
|
|
||||||
pole_to_del.delete()
|
|
||||||
except ProtectedError:
|
|
||||||
pole_to_del.status = 'archived'
|
|
||||||
pole_to_del.updated_by = request.user
|
|
||||||
pole_to_del.save()
|
|
||||||
messages.warning(request, f"Le poteau {pole_to_del.code} n'a pas pu être supprimé car d'autres éléments y font référence ; il a été archivé à la place.")
|
|
||||||
|
|
||||||
# Fetch active poles to match parent relation for lanterns/detectors if not resolved above
|
# Fetch active poles to match parent relation for lanterns/detectors if not resolved above
|
||||||
active_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')}
|
active_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')}
|
||||||
|
|
@ -5541,18 +5415,6 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
new_cable.save()
|
new_cable.save()
|
||||||
existing.replaced_by = new_cable
|
existing.replaced_by = new_cable
|
||||||
existing.save()
|
existing.save()
|
||||||
elif action == "archive" and existing:
|
|
||||||
existing.status = 'archived'
|
|
||||||
existing.updated_by = request.user
|
|
||||||
existing.save()
|
|
||||||
elif action == "delete" and existing:
|
|
||||||
try:
|
|
||||||
existing.delete()
|
|
||||||
except ProtectedError:
|
|
||||||
existing.status = 'archived'
|
|
||||||
existing.updated_by = request.user
|
|
||||||
existing.save()
|
|
||||||
messages.warning(request, f"Le câble {existing.code} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
|
|
||||||
else:
|
else:
|
||||||
# 'keep' (Ne rien faire)
|
# 'keep' (Ne rien faire)
|
||||||
if existing:
|
if existing:
|
||||||
|
|
@ -5570,17 +5432,6 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
TrafficLightCable.objects.filter(intersection=intersection, code=code, status='active').update(
|
TrafficLightCable.objects.filter(intersection=intersection, code=code, status='active').update(
|
||||||
status='archived', updated_by=request.user
|
status='archived', updated_by=request.user
|
||||||
)
|
)
|
||||||
elif action == "delete":
|
|
||||||
db_id = request.POST.get(f"db_id_cable_{code}")
|
|
||||||
qs = TrafficLightCable.objects.filter(pk=db_id) if db_id else TrafficLightCable.objects.filter(intersection=intersection, code=code)
|
|
||||||
for cable_to_del in qs:
|
|
||||||
try:
|
|
||||||
cable_to_del.delete()
|
|
||||||
except ProtectedError:
|
|
||||||
cable_to_del.status = 'archived'
|
|
||||||
cable_to_del.updated_by = request.user
|
|
||||||
cable_to_del.save()
|
|
||||||
messages.warning(request, f"Le câble {cable_to_del.code} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
|
|
||||||
|
|
||||||
# --- 3. Lanterns integration ---
|
# --- 3. Lanterns integration ---
|
||||||
for dl in deduced["lanterns"]:
|
for dl in deduced["lanterns"]:
|
||||||
|
|
@ -5642,18 +5493,6 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
)
|
)
|
||||||
existing.replaced_by = new_lantern
|
existing.replaced_by = new_lantern
|
||||||
existing.save()
|
existing.save()
|
||||||
elif action == "archive" and existing:
|
|
||||||
existing.status = 'archived'
|
|
||||||
existing.updated_by = request.user
|
|
||||||
existing.save()
|
|
||||||
elif action == "delete" and existing:
|
|
||||||
try:
|
|
||||||
existing.delete()
|
|
||||||
except ProtectedError:
|
|
||||||
existing.status = 'archived'
|
|
||||||
existing.updated_by = request.user
|
|
||||||
existing.save()
|
|
||||||
messages.warning(request, f"La lanterne {existing.code} n'a pas pu être supprimée car elle est référencée ; elle a été archivée à la place.")
|
|
||||||
else:
|
else:
|
||||||
# 'keep'
|
# 'keep'
|
||||||
if existing:
|
if existing:
|
||||||
|
|
@ -5673,17 +5512,6 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code, status='active').update(
|
TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code, status='active').update(
|
||||||
status='archived', updated_by=request.user
|
status='archived', updated_by=request.user
|
||||||
)
|
)
|
||||||
elif action == "delete":
|
|
||||||
db_id = request.POST.get(f"db_id_lantern_{code}")
|
|
||||||
qs = TrafficLightLantern.objects.filter(pk=db_id) if db_id else TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code)
|
|
||||||
for lan_to_del in qs:
|
|
||||||
try:
|
|
||||||
lan_to_del.delete()
|
|
||||||
except ProtectedError:
|
|
||||||
lan_to_del.status = 'archived'
|
|
||||||
lan_to_del.updated_by = request.user
|
|
||||||
lan_to_del.save()
|
|
||||||
messages.warning(request, f"La lanterne {lan_to_del.code} n'a pas pu être supprimée car elle est référencée ; elle a été archivée à la place.")
|
|
||||||
|
|
||||||
# --- 4. Detectors integration ---
|
# --- 4. Detectors integration ---
|
||||||
for dd in deduced["detectors"]:
|
for dd in deduced["detectors"]:
|
||||||
|
|
@ -5753,18 +5581,6 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
)
|
)
|
||||||
existing.replaced_by = new_det
|
existing.replaced_by = new_det
|
||||||
existing.save()
|
existing.save()
|
||||||
elif action == "archive" and existing:
|
|
||||||
existing.status = 'archived'
|
|
||||||
existing.updated_by = request.user
|
|
||||||
existing.save()
|
|
||||||
elif action == "delete" and existing:
|
|
||||||
try:
|
|
||||||
existing.delete()
|
|
||||||
except ProtectedError:
|
|
||||||
existing.status = 'archived'
|
|
||||||
existing.updated_by = request.user
|
|
||||||
existing.save()
|
|
||||||
messages.warning(request, f"Le détecteur {existing.code} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
|
|
||||||
else:
|
else:
|
||||||
# 'keep'
|
# 'keep'
|
||||||
if existing:
|
if existing:
|
||||||
|
|
@ -5785,17 +5601,6 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
TrafficLightDetector.objects.filter(intersection=intersection, code=code, status='active').update(
|
TrafficLightDetector.objects.filter(intersection=intersection, code=code, status='active').update(
|
||||||
status='archived', updated_by=request.user
|
status='archived', updated_by=request.user
|
||||||
)
|
)
|
||||||
elif action == "delete":
|
|
||||||
db_id = request.POST.get(f"db_id_detector_{code}")
|
|
||||||
qs = TrafficLightDetector.objects.filter(pk=db_id) if db_id else TrafficLightDetector.objects.filter(intersection=intersection, code=code)
|
|
||||||
for det_to_del in qs:
|
|
||||||
try:
|
|
||||||
det_to_del.delete()
|
|
||||||
except ProtectedError:
|
|
||||||
det_to_del.status = 'archived'
|
|
||||||
det_to_del.updated_by = request.user
|
|
||||||
det_to_del.save()
|
|
||||||
messages.warning(request, f"Le détecteur {det_to_del.code} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
|
|
||||||
|
|
||||||
# --- 5. Other associated assets integration ---
|
# --- 5. Other associated assets integration ---
|
||||||
for key in request.POST.keys():
|
for key in request.POST.keys():
|
||||||
|
|
@ -5805,39 +5610,36 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
asset_type, asset_id_str = suffix.split("_", 1)
|
asset_type, asset_id_str = suffix.split("_", 1)
|
||||||
is_selected = request.POST.get(f"selected_other_{suffix}") == "on"
|
is_selected = request.POST.get(f"selected_other_{suffix}") == "on"
|
||||||
action = request.POST.get(key, "keep")
|
action = request.POST.get(key, "keep")
|
||||||
if is_selected and action in ("archive", "delete"):
|
if is_selected and action == "archive":
|
||||||
try:
|
try:
|
||||||
asset_id = int(asset_id_str)
|
asset_id = int(asset_id_str)
|
||||||
model_map = {
|
if asset_type == "signpanel":
|
||||||
"signpanel": SignPanel,
|
from assets.models.sign import SignPanel
|
||||||
"itsasset": ITSGeoAsset,
|
SignPanel.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
"controller": TrafficLightController,
|
elif asset_type == "itsasset":
|
||||||
"networkhardware": TrafficLightNetworkHardware,
|
from assets.models.its import ITSGeoAsset
|
||||||
"voltagehardware": TrafficLightVoltageHardware,
|
ITSGeoAsset.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
"electricalcabinet": TrafficLightElectricalCabinet,
|
elif asset_type == "controller":
|
||||||
"accessory": TrafficLightAccessory,
|
TrafficLightController.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
"radar": TrafficLightRadar,
|
from assets.models.trafficlights import TrafficLightControllerHardware
|
||||||
"programming": TrafficLightProgramming,
|
|
||||||
"lightfixture": PublicLightingLightFixture,
|
|
||||||
}
|
|
||||||
model_cls = model_map.get(asset_type)
|
|
||||||
if model_cls:
|
|
||||||
if action == "archive":
|
|
||||||
model_cls.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
|
||||||
if asset_type == "controller":
|
|
||||||
TrafficLightControllerHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
|
TrafficLightControllerHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
|
||||||
TrafficLightNetworkHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
|
TrafficLightNetworkHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
|
||||||
TrafficLightVoltageHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
|
TrafficLightVoltageHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
|
||||||
elif action == "delete":
|
elif asset_type == "networkhardware":
|
||||||
obj = model_cls.objects.filter(pk=asset_id).first()
|
TrafficLightNetworkHardware.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
if obj:
|
elif asset_type == "voltagehardware":
|
||||||
try:
|
TrafficLightVoltageHardware.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
obj.delete()
|
elif asset_type == "electricalcabinet":
|
||||||
except ProtectedError:
|
TrafficLightElectricalCabinet.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
obj.status = 'archived'
|
elif asset_type == "accessory":
|
||||||
obj.updated_by = request.user
|
TrafficLightAccessory.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
obj.save()
|
elif asset_type == "radar":
|
||||||
messages.warning(request, f"L'équipement {obj} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
|
TrafficLightRadar.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
|
elif asset_type == "programming":
|
||||||
|
TrafficLightProgramming.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
|
elif asset_type == "lightfixture":
|
||||||
|
from assets.models.publiclighting import PublicLightingLightFixture
|
||||||
|
PublicLightingLightFixture.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
|
||||||
except (ValueError, TypeError, Exception):
|
except (ValueError, TypeError, Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -506,7 +506,7 @@ class UserThematicsInline(admin.TabularInline):
|
||||||
'thematic', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', 'can_inspect',
|
'thematic', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', 'can_inspect',
|
||||||
'can_view_interventions', 'can_edit_interventions', 'can_edit_locations',
|
'can_view_interventions', 'can_edit_interventions', 'can_edit_locations',
|
||||||
'can_process_observations',
|
'can_process_observations',
|
||||||
'can_view_projects', 'can_edit_projects', 'can_edit_management_zones',
|
'can_view_projects', 'can_edit_projects',
|
||||||
'status_matrix',
|
'status_matrix',
|
||||||
]
|
]
|
||||||
readonly_fields = ['status_matrix']
|
readonly_fields = ['status_matrix']
|
||||||
|
|
@ -598,7 +598,7 @@ class UserAssetTypeAccessInline(admin.TabularInline):
|
||||||
|
|
||||||
@admin.register(UserThematics)
|
@admin.register(UserThematics)
|
||||||
class UserThematicsAdmin(admin.ModelAdmin):
|
class UserThematicsAdmin(admin.ModelAdmin):
|
||||||
list_display = ('user_config', 'thematic', 'can_view_interventions', 'can_edit_interventions', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', 'can_inspect', 'can_process_observations', 'can_edit_management_zones')
|
list_display = ('user_config', 'thematic', 'can_view_interventions', 'can_edit_interventions', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', 'can_inspect', 'can_process_observations')
|
||||||
list_filter = ('thematic',)
|
list_filter = ('thematic',)
|
||||||
search_fields = ('user_config__user__username', 'thematic__name_fr', 'thematic__name_nl')
|
search_fields = ('user_config__user__username', 'thematic__name_fr', 'thematic__name_nl')
|
||||||
raw_id_fields = ['user_config']
|
raw_id_fields = ['user_config']
|
||||||
|
|
@ -620,8 +620,8 @@ class UserContractAccessAdmin(admin.ModelAdmin):
|
||||||
@admin.register(UserConfig)
|
@admin.register(UserConfig)
|
||||||
class UserConfigAdmin(admin.ModelAdmin):
|
class UserConfigAdmin(admin.ModelAdmin):
|
||||||
list_display = ("user", "display_contracts", "display_thematics", "display_views", "is_intern", "display_roles", "company", "display_member_teams", "duplicate_button")
|
list_display = ("user", "display_contracts", "display_thematics", "display_views", "is_intern", "display_roles", "company", "display_member_teams", "duplicate_button")
|
||||||
list_filter = ("thematics", "roles", "company", "user__companymember__teams", "contracts", "can_view_all_posts", "can_view_all_prices", "accessible_views", "auto_logout_hours")
|
list_filter = ("thematics", "roles", "company", "user__companymember__teams", "contracts", "accessible_views", "auto_logout_hours")
|
||||||
search_fields = ("user__username", "user__first_name", "user__last_name", "contracts__contract_number")
|
search_fields = ("user__username", "contracts__contract_number")
|
||||||
inlines = [UserThematicsInline, UserContractAccessInline, UserWarehouseAccessInline, UserWarehouseLocationAccessInline, UserCompanyTeamAccessInline, UserAssetTypeAccessInline, UserAssetAccessInline]
|
inlines = [UserThematicsInline, UserContractAccessInline, UserWarehouseAccessInline, UserWarehouseLocationAccessInline, UserCompanyTeamAccessInline, UserAssetTypeAccessInline, UserAssetAccessInline]
|
||||||
filter_horizontal = ("linked_configs", "accessible_views")
|
filter_horizontal = ("linked_configs", "accessible_views")
|
||||||
actions = ["duplicate_config_action", "apply_config_from_action", "assign_company_action"]
|
actions = ["duplicate_config_action", "apply_config_from_action", "assign_company_action"]
|
||||||
|
|
@ -637,10 +637,6 @@ class UserConfigAdmin(admin.ModelAdmin):
|
||||||
'fields': ('accessible_views',),
|
'fields': ('accessible_views',),
|
||||||
'description': "Configurer les vues auxquelles l'utilisateur a accès."
|
'description': "Configurer les vues auxquelles l'utilisateur a accès."
|
||||||
}),
|
}),
|
||||||
('Permissions globales / Contrats', {
|
|
||||||
'fields': ('can_view_all_posts', 'can_view_all_prices'),
|
|
||||||
'description': "Permet de consulter les prestations et/ou les prix sur tous les contrats sans configuration contractuelle individuelle."
|
|
||||||
}),
|
|
||||||
('eSign / Signalisation', {
|
('eSign / Signalisation', {
|
||||||
'fields': ('esign_can_edit', 'esign_can_manage_rcp', 'esign_can_validate_rcp', 'esign_organizations'),
|
'fields': ('esign_can_edit', 'esign_can_manage_rcp', 'esign_can_validate_rcp', 'esign_organizations'),
|
||||||
'description': "Permissions spécifiques pour l'application eSign / Signalisation."
|
'description': "Permissions spécifiques pour l'application eSign / Signalisation."
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -541,20 +541,3 @@ msgstr ""
|
||||||
#: common/views.py:644
|
#: common/views.py:644
|
||||||
msgid "Erreur lors de la lecture du document"
|
msgid "Erreur lors de la lecture du document"
|
||||||
msgstr "Erreur lors de la lecture du document"
|
msgstr "Erreur lors de la lecture du document"
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Dézoomer"
|
|
||||||
msgstr "Zoom out"
|
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Zoomer"
|
|
||||||
msgstr "Zoom in"
|
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Réinitialiser"
|
|
||||||
msgstr "Reset"
|
|
||||||
|
|
||||||
#: common/views.py:683
|
|
||||||
msgid "Format de fond de plan invalide"
|
|
||||||
msgstr "Invalid basemap format"
|
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -133,11 +133,3 @@ msgstr "@"
|
||||||
#: loko/common/static/common/jquery-ui.min.js:6
|
#: loko/common/static/common/jquery-ui.min.js:6
|
||||||
msgid "!"
|
msgid "!"
|
||||||
msgstr "!"
|
msgstr "!"
|
||||||
|
|
||||||
#: loko/common/static/common/address_search_control.js:148
|
|
||||||
msgid "Rechercher une adresse…"
|
|
||||||
msgstr "Search an address…"
|
|
||||||
|
|
||||||
msgid "Rechercher une adresse..."
|
|
||||||
msgstr "Search an address..."
|
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -527,20 +527,3 @@ msgstr "Accès interdit"
|
||||||
#: common/views.py:644
|
#: common/views.py:644
|
||||||
msgid "Erreur lors de la lecture du document"
|
msgid "Erreur lors de la lecture du document"
|
||||||
msgstr "Erreur lors de la lecture du document"
|
msgstr "Erreur lors de la lecture du document"
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Dézoomer"
|
|
||||||
msgstr "Dézoomer"
|
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Zoomer"
|
|
||||||
msgstr "Zoomer"
|
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Réinitialiser"
|
|
||||||
msgstr "Réinitialiser"
|
|
||||||
|
|
||||||
#: common/views.py:683
|
|
||||||
msgid "Format de fond de plan invalide"
|
|
||||||
msgstr "Format de fond de plan invalide"
|
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -133,11 +133,3 @@ msgstr "@"
|
||||||
#: loko/common/static/common/jquery-ui.min.js:6
|
#: loko/common/static/common/jquery-ui.min.js:6
|
||||||
msgid "!"
|
msgid "!"
|
||||||
msgstr "!"
|
msgstr "!"
|
||||||
|
|
||||||
#: loko/common/static/common/address_search_control.js:148
|
|
||||||
msgid "Rechercher une adresse…"
|
|
||||||
msgstr "Rechercher une adresse…"
|
|
||||||
|
|
||||||
msgid "Rechercher une adresse..."
|
|
||||||
msgstr "Rechercher une adresse..."
|
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -527,20 +527,3 @@ msgstr ""
|
||||||
#: common/views.py:644
|
#: common/views.py:644
|
||||||
msgid "Erreur lors de la lecture du document"
|
msgid "Erreur lors de la lecture du document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Dézoomer"
|
|
||||||
msgstr "Uitzoomen"
|
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Zoomer"
|
|
||||||
msgstr "Inzoomen"
|
|
||||||
|
|
||||||
#: common/templates/common/components/document_viewer.html
|
|
||||||
msgid "Réinitialiser"
|
|
||||||
msgstr "Herstellen"
|
|
||||||
|
|
||||||
#: common/views.py:683
|
|
||||||
msgid "Format de fond de plan invalide"
|
|
||||||
msgstr "Ongeldig basiskaartformaat"
|
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue