up setting plein air
This commit is contained in:
parent
4f6a388129
commit
a6331c8ced
5 changed files with 401 additions and 291 deletions
|
|
@ -17,6 +17,7 @@ export class AllEvents implements OnInit, OnDestroy {
|
||||||
@Output() select = new EventEmitter<any>();
|
@Output() select = new EventEmitter<any>();
|
||||||
@Output() pickCoords = new EventEmitter<[number, number]>();
|
@Output() pickCoords = new EventEmitter<[number, number]>();
|
||||||
@Output() selection = new EventEmitter<Array<string | number>>();
|
@Output() selection = new EventEmitter<Array<string | number>>();
|
||||||
|
@Output() mapMove = new EventEmitter<{ minLng: number, minLat: number, maxLng: number, maxLat: number }>();
|
||||||
|
|
||||||
@ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef<HTMLDivElement>;
|
@ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
|
|
@ -165,16 +166,18 @@ export class AllEvents implements OnInit, OnDestroy {
|
||||||
this.pickCoords.emit(coords);
|
this.pickCoords.emit(coords);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Écouter les changements de vue pour mettre à jour l'URL
|
// Écouter les changements de vue pour mettre à jour l'URL et émettre la bbox
|
||||||
this.map.on('moveend', () => {
|
this.map.on('moveend', () => {
|
||||||
if (this.mapInitialized) {
|
if (this.mapInitialized) {
|
||||||
this.updateUrlFromMap();
|
this.updateUrlFromMap();
|
||||||
|
this.emitCurrentBbox();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.map.on('zoomend', () => {
|
this.map.on('zoomend', () => {
|
||||||
if (this.mapInitialized) {
|
if (this.mapInitialized) {
|
||||||
this.updateUrlFromMap();
|
this.updateUrlFromMap();
|
||||||
|
this.emitCurrentBbox();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -634,4 +637,18 @@ export class AllEvents implements OnInit, OnDestroy {
|
||||||
private escapeHtml(s: string): string {
|
private escapeHtml(s: string): string {
|
||||||
return s.replace(/[&<>"]+/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c] as string));
|
return s.replace(/[&<>"]+/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c] as string));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitCurrentBbox() {
|
||||||
|
if (!this.map) return;
|
||||||
|
|
||||||
|
const bounds = this.map.getBounds();
|
||||||
|
const bbox = {
|
||||||
|
minLng: bounds.getWest(),
|
||||||
|
minLat: bounds.getSouth(),
|
||||||
|
maxLng: bounds.getEast(),
|
||||||
|
maxLat: bounds.getNorth()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mapMove.emit(bbox);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,344 +1,384 @@
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
|
||||||
@if(showOptions){
|
|
||||||
<div class="aside">
|
<div class="aside">
|
||||||
|
<div class="toolbar">
|
||||||
|
@if (isLoading) {
|
||||||
|
<span class="loading-indicator">⏳</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if(showOptions){
|
||||||
|
|
||||||
<div class="aside-content">
|
<div class="aside-content">
|
||||||
<div class="toolbar">
|
|
||||||
<!-- <span class="loading-indicator">⏳</span> -->
|
|
||||||
@if (isLoading) {
|
|
||||||
<span class="loading-indicator">⏳</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="filters">
|
|
||||||
|
|
||||||
<label (click)="showFilters = !showFilters">
|
|
||||||
Filtre rapide
|
|
||||||
@if (showFilters) {
|
|
||||||
<span>▼</span>
|
|
||||||
} @else {
|
|
||||||
<span>▶</span>
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="control-group" id="pleinAirMode">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" [(ngModel)]="pleinAirMode" (change)="togglePleinAir()">
|
<div class="filters">
|
||||||
Mode plein air
|
|
||||||
|
<label (click)="showFilters = !showFilters">
|
||||||
|
Filtre rapide
|
||||||
|
@if (showFilters) {
|
||||||
|
<span>▼</span>
|
||||||
|
} @else {
|
||||||
|
<span>▶</span>
|
||||||
|
}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
</div>
|
<div class="control-group" id="pleinAirMode">
|
||||||
|
<label>
|
||||||
<div class="filters-group">
|
<input type="checkbox" [(ngModel)]="pleinAirMode" (change)="togglePleinAir()">
|
||||||
@if (showFilters) {
|
Mode plein air
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters-group">
|
||||||
|
@if (showFilters) {
|
||||||
<span class="muted">{{filteredFeatures.length}} évènements chargés</span>
|
<span class="muted">{{filteredFeatures.length}} évènements chargés</span>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Jours à venir</label>
|
<label>Jours à venir</label>
|
||||||
<input
|
<input type="number" class="input" [(ngModel)]="daysAhead" (ngModelChange)="onDaysAheadChange()" min="1"
|
||||||
type="number"
|
max="30" placeholder="7">
|
||||||
class="input"
|
|
||||||
[(ngModel)]="daysAhead"
|
|
||||||
(ngModelChange)="onDaysAheadChange()"
|
|
||||||
min="1"
|
|
||||||
max="30"
|
|
||||||
placeholder="7">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" [(ngModel)]="autoReloadEnabled" (change)="toggleAutoReload()">
|
||||||
|
Rechargement auto (1min)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" [(ngModel)]="useBboxFilter" (change)="onBboxFilterToggle()">
|
||||||
|
Filtrer par zone visible (bbox)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<input class="input" type="text" placeholder="Rechercher..." [(ngModel)]="searchText"
|
||||||
|
(ngModelChange)="onSearchChange()">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>
|
<label>Période (début / fin)</label>
|
||||||
<input
|
<div style="display:flex; gap:6px;">
|
||||||
type="checkbox"
|
<input class="input" type="date" [(ngModel)]="startDateStr">
|
||||||
[(ngModel)]="autoReloadEnabled"
|
<input class="input" type="date" [(ngModel)]="endDateStr">
|
||||||
(change)="toggleAutoReload()">
|
</div>
|
||||||
Rechargement auto (1min)
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>
|
<button class="btn" (click)="onQuickSearchSubmit()">Rechercher</button>
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[(ngModel)]="useBboxFilter"
|
|
||||||
(change)="onBboxFilterToggle()">
|
|
||||||
Filtrer par zone visible (bbox)
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<app-what-filter [label]="'Filtrer par type d\'événement'" [available]="availableWhatTypes"
|
||||||
|
[selected]="selectedWhatFilter"
|
||||||
|
(selectedChange)="selectedWhatFilter = $event; onWhatFilterChange()"></app-what-filter>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<input class="input" type="text" placeholder="Rechercher..." [(ngModel)]="searchText" (ngModelChange)="onSearchChange()">
|
|
||||||
<div class="control-group">
|
<!-- <app-osm></app-osm>
|
||||||
<label>Période (début / fin)</label>
|
|
||||||
<div style="display:flex; gap:6px;">
|
<app-menu></app-menu> -->
|
||||||
<input class="input" type="date" [(ngModel)]="startDateStr">
|
<hr>
|
||||||
<input class="input" type="date" [(ngModel)]="endDateStr">
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="control-group">
|
|
||||||
<button class="btn" (click)="onQuickSearchSubmit()">Rechercher</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<app-what-filter [label]="'Filtrer par type d\'événement'" [available]="availableWhatTypes" [selected]="selectedWhatFilter" (selectedChange)="selectedWhatFilter = $event; onWhatFilterChange()"></app-what-filter>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- <app-unlocated-events [events]="filteredFeatures"></app-unlocated-events> -->
|
||||||
|
|
||||||
<!-- <app-osm></app-osm>
|
@if(showEditForm){
|
||||||
|
<div class="guide">
|
||||||
<app-menu></app-menu> -->
|
<h3>Guide</h3>
|
||||||
<hr>
|
<ul>
|
||||||
|
<li> Créer un évènement: Cliquez sur le bouton "+" pour créer un nouvel évènement. Sélectionnez un preset,
|
||||||
|
remplissez les informations, cliquez quelque part sur la carte pour définir un emplacement. Puis appuyez sur
|
||||||
|
créer.</li>
|
||||||
|
<li> Mettre à jour un évènement: Sélectionnez un évènement sur la carte ou dans la liste pour le modifier.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@if(!pleinAirMode){
|
||||||
|
<app-edit-form [selected]="selected" (saved)="onSaved($event)" (created)="onCreated($event)"
|
||||||
|
(deleted)="onDeleted($event)" (canceled)="onCanceled()"></app-edit-form>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
<div id="fixed_actions">
|
||||||
|
<button class="button btn btn-primary" (click)="createEvent()" title="Créer un évènement">+ nouvel
|
||||||
<!-- <app-unlocated-events [events]="filteredFeatures"></app-unlocated-events> -->
|
évènement</button>
|
||||||
|
<button class="button" (click)="toggleView()" title="Basculer carte / tableau">📊</button>
|
||||||
<hr>
|
<div class="downloaders">
|
||||||
@if(showEditForm){
|
<button class="button" (click)="downloadGeoJSON()" title="Télécharger GeoJSON">📥 GeoJSON</button>
|
||||||
|
<button class="button" (click)="downloadCSV()" title="Télécharger CSV">📥 CSV</button>
|
||||||
<div class="guide">
|
</div>
|
||||||
<h3>Guide</h3>
|
<div class="selectors">
|
||||||
<ul>
|
<button class="button" [class.active]="selectionMode==='rectangle'" (click)="startRectSelection()"
|
||||||
<li> Créer un évènement: Cliquez sur le bouton "+" pour créer un nouvel évènement. Sélectionnez un preset, remplissez les informations, cliquez quelque part sur la carte pour définir un emplacement. Puis appuyez sur créer.</li>
|
title="Sélection rectangulaire">▭</button>
|
||||||
<li> Mettre à jour un évènement: Sélectionnez un évènement sur la carte ou dans la liste pour le modifier.</li>
|
<button class="button" [class.active]="selectionMode==='polygon'" (click)="startPolySelection()"
|
||||||
</ul>
|
title="Sélection polygone">⬠</button>
|
||||||
</div>
|
@if (selectedIds.length) {
|
||||||
@if(!pleinAirMode){
|
|
||||||
<app-edit-form [selected]="selected" (saved)="onSaved($event)" (created)="onCreated($event)" (deleted)="onDeleted($event)" (canceled)="onCanceled()"></app-edit-form>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<div id="fixed_actions">
|
|
||||||
<button class="button btn btn-primary" (click)="createEvent()" title="Créer un évènement">+ nouvel évènement</button>
|
|
||||||
<button class="button" (click)="toggleView()" title="Basculer carte / tableau">📊</button>
|
|
||||||
<div class="downloaders">
|
|
||||||
<button class="button" (click)="downloadGeoJSON()" title="Télécharger GeoJSON">📥 GeoJSON</button>
|
|
||||||
<button class="button" (click)="downloadCSV()" title="Télécharger CSV">📥 CSV</button>
|
|
||||||
</div>
|
|
||||||
<div class="selectors">
|
|
||||||
<button class="button" [class.active]="selectionMode==='rectangle'" (click)="startRectSelection()" title="Sélection rectangulaire">▭</button>
|
|
||||||
<button class="button" [class.active]="selectionMode==='polygon'" (click)="startPolySelection()" title="Sélection polygone">⬠</button>
|
|
||||||
@if (selectedIds.length) {
|
|
||||||
<span class="muted">{{selectedIds.length}} sélectionné(s)</span>
|
<span class="muted">{{selectedIds.length}} sélectionné(s)</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@if(selected !== null){
|
||||||
|
<div class="selected">
|
||||||
|
<h3> sélectionné: {{selected.properties.name}} {{selected.properties.title}} {{selected.properties.label}}</h3>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>label</td>
|
||||||
|
<td>{{selected.properties.label}}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>name</td>
|
||||||
|
<td>{{selected.properties.name}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>title</td>
|
||||||
|
<td>{{selected.properties.title}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>description</td>
|
||||||
|
<td>{{selected.properties.description}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>what</td>
|
||||||
|
<td>{{selected.properties.what}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>where</td>
|
||||||
|
<td>{{selected.properties.where}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>lat</td>
|
||||||
|
<td>{{selected.properties.lat}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>lon</td>
|
||||||
|
<td>{{selected.properties.lon}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>wikidata</td>
|
||||||
|
<td>{{selected.properties.wikidata}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>featureType</td>
|
||||||
|
<td>{{selected.properties.featureType}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>type</td>
|
||||||
|
<td>{{selected.properties.type}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>start</td>
|
||||||
|
<td>{{selected.properties.start}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>stop</td>
|
||||||
|
<td>{{selected.properties.stop}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>source</td>
|
||||||
|
<td>{{selected.properties.source}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>createdate</td>
|
||||||
|
<td>{{selected.properties.createdate}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>lastupdate</td>
|
||||||
|
<td>{{selected.properties.lastupdate}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>type</td>
|
||||||
|
<td>{{selected.properties.type}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@if (selectedIds.length) {
|
||||||
|
<div class="batch-panel">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="row">
|
||||||
|
<label>Action de masse</label>
|
||||||
|
<select class="input" [(ngModel)]="batchAction">
|
||||||
|
<option value="none">Choisir...</option>
|
||||||
|
<option value="changeWhat">Changer le type d'évènement (what)</option>
|
||||||
|
<option value="setField">Remplacer une propriété</option>
|
||||||
|
<option value="delete">Supprimer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
@if (batchAction==='changeWhat') {
|
||||||
|
<div class="row">
|
||||||
|
<label>Nouveau "what"</label>
|
||||||
|
<input class="input" type="text" [(ngModel)]="batchWhat" placeholder="ex: traffic.roadwork" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (batchAction==='setField') {
|
||||||
|
<div class="row">
|
||||||
|
<label>Clé de propriété</label>
|
||||||
|
<input class="input" type="text" [(ngModel)]="batchFieldKey" placeholder="ex: where" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Nouvelle valeur</label>
|
||||||
|
<input class="input" type="text" [(ngModel)]="batchFieldValue" placeholder="ex: Paris, 12e" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn" (click)="applyBatch()" [disabled]="batchAction==='none'">Appliquer</button>
|
||||||
|
<button class="btn btn-ghost" (click)="clearSelection()">Annuler</button>
|
||||||
|
</div>
|
||||||
|
@if (batchSummary) {
|
||||||
|
<div class="summary">
|
||||||
|
<span>Succès: {{batchSummary.success}}</span>
|
||||||
|
<span>Échecs: {{batchSummary.failed}}</span>
|
||||||
|
<span>Erreurs réseau: {{batchSummary.networkErrors}}</span>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@if(selected !== null){
|
|
||||||
<div class="selected">
|
|
||||||
|
|
||||||
<h3> sélectionné: {{selected.properties.name}}</h3>
|
|
||||||
|
|
||||||
{{selected.properties.label}}
|
|
||||||
<br>
|
|
||||||
{{selected.properties.label}}
|
|
||||||
<br>
|
|
||||||
{{selected.properties.what}}
|
|
||||||
<br>
|
|
||||||
{{selected.properties.where}}
|
|
||||||
<br>
|
|
||||||
{{selected.properties.lat}}
|
|
||||||
<br>
|
|
||||||
{{selected.properties.lon}}
|
|
||||||
|
|
||||||
<br>
|
|
||||||
{{selected.properties.wikidata}}
|
|
||||||
<br>
|
|
||||||
{{selected.properties.featureType}}
|
|
||||||
<br>
|
|
||||||
{{selected.properties.type}}
|
|
||||||
<br>
|
|
||||||
start:
|
|
||||||
{{selected.properties.start}}
|
|
||||||
<br>
|
|
||||||
end:
|
|
||||||
{{selected.properties.stop}}
|
|
||||||
<br>
|
|
||||||
source
|
|
||||||
{{selected.properties.source}}
|
|
||||||
<br>
|
|
||||||
description:
|
|
||||||
{{selected.properties.description}}
|
|
||||||
<br>
|
|
||||||
createdate:
|
|
||||||
{{selected.properties.createdate}}
|
|
||||||
<br>
|
|
||||||
lastupdate:
|
|
||||||
{{selected.properties.lastupdate}}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="main {{showOptions? 'is-small' : 'is-full'}}">
|
|
||||||
<button class="button toggle-options" (click)="showOptions = !showOptions">
|
|
||||||
Options
|
|
||||||
</button>
|
|
||||||
@if (pleinAirMode) {
|
|
||||||
<div class="quick-actions" style="margin-top:8px; display:flex; gap:6px; flex-wrap:wrap;">
|
|
||||||
<button class="btn" (click)="quickCreate('traffic.contestation')">🚩 Contester</button>
|
|
||||||
<button class="btn" (click)="quickCreate('traffic.interruption')">⛓️ Interruption</button>
|
|
||||||
<button class="btn" (click)="quickCreate('traffic.wrong_way')">⛖ Détourné</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (toasts.length) {
|
|
||||||
<div class="toaster" style="position:fixed;right:16px;top:16px;display:flex;flex-direction:column;gap:8px;z-index:1000;">
|
|
||||||
@for (t of toasts; track t.id) {
|
}
|
||||||
<div class="toast" [class.success]="t.type==='success'" [class.error]="t.type==='error'" [class.info]="t.type==='info'" style="padding:10px 12px;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,0.15);background:#fff;min-width:200px;">
|
</div>
|
||||||
|
|
||||||
|
<div class="main {{showOptions? 'is-small' : 'is-full'}}">
|
||||||
|
<button class="button toggle-options" (click)="showOptions = !showOptions">
|
||||||
|
Options
|
||||||
|
</button>
|
||||||
|
@if (pleinAirMode) {
|
||||||
|
<div class="quick-actions" style="margin-top:8px; display:flex; gap:6px; flex-wrap:wrap;">
|
||||||
|
<button class="btn" (click)="quickCreate('traffic.contestation')">🚩 Contester</button>
|
||||||
|
<button class="btn" (click)="quickCreate('traffic.interruption')">⛓️ Interruption</button>
|
||||||
|
<button class="btn" (click)="quickCreate('traffic.wrong_way')">⛖ Détourné</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (toasts.length) {
|
||||||
|
<div class="toaster"
|
||||||
|
style="position:fixed;right:16px;top:16px;display:flex;flex-direction:column;gap:8px;z-index:1000;">
|
||||||
|
@for (t of toasts; track t.id) {
|
||||||
|
<div class="toast" [class.success]="t.type==='success'" [class.error]="t.type==='error'"
|
||||||
|
[class.info]="t.type==='info'"
|
||||||
|
style="padding:10px 12px;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,0.15);background:#fff;min-width:200px;">
|
||||||
{{t.message}}
|
{{t.message}}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
@if (theme()) {
|
||||||
}
|
<div class="subtheme-bar">
|
||||||
@if (theme()) {
|
<div class="help">Thème: {{ theme() }} — Cliquez sur la carte pour définir des coordonnées puis créez un
|
||||||
<div class="subtheme-bar">
|
évènement du sous-thème choisi.</div>
|
||||||
<div class="help">Thème: {{ theme() }} — Cliquez sur la carte pour définir des coordonnées puis créez un évènement du sous-thème choisi.</div>
|
<div class="chips">
|
||||||
<div class="chips">
|
@for (t of subthemes; track t.key) {
|
||||||
@for (t of subthemes; track t.key) {
|
|
||||||
<button class="chip" [class.active]="activeSubtheme()===t.key" (click)="activeSubtheme.set(t.key)">
|
<button class="chip" [class.active]="activeSubtheme()===t.key" (click)="activeSubtheme.set(t.key)">
|
||||||
<span class="emoji">{{t.emoji}}</span>
|
<span class="emoji">{{t.emoji}}</span>
|
||||||
<span>{{t.label}}</span>
|
<span>{{t.label}}</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
@if (!showTable && !showUnlocatedList) {
|
||||||
@if (!showTable && !showUnlocatedList) {
|
<div class="map">
|
||||||
<div class="map">
|
<app-all-events [features]="filteredFeatures" [selected]="selected" [selectMode]="selectionMode"
|
||||||
<app-all-events [features]="filteredFeatures" [selected]="selected" [selectMode]="selectionMode" (selection)="onSelection($event)" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"></app-all-events>
|
(selection)="onSelection($event)" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"
|
||||||
</div>
|
(mapMove)="onMapMove($event)"></app-all-events>
|
||||||
} @else if (showUnlocatedList) {
|
</div>
|
||||||
<div class="table-wrapper" style="overflow:auto;height:100%;">
|
} @else if (showUnlocatedList) {
|
||||||
<div class="unlocated-layout">
|
<div class="table-wrapper" style="overflow:auto;height:100%;">
|
||||||
<aside class="agenda-sidebar">
|
<div class="unlocated-layout">
|
||||||
<div class="sidebar-header">
|
<aside class="agenda-sidebar">
|
||||||
<h3>Sans lieu / en ligne</h3>
|
<div class="sidebar-header">
|
||||||
<small>{{unlocatedOrOnline.length}} évènements</small>
|
<h3>Sans lieu / en ligne</h3>
|
||||||
</div>
|
<small>{{unlocatedOrOnline.length}} évènements</small>
|
||||||
<div class="day-groups">
|
</div>
|
||||||
<ul class="event-list">
|
<div class="day-groups">
|
||||||
@for (f of unlocatedOrOnline; track f.id) {
|
<ul class="event-list">
|
||||||
<li class="event-item" (click)="onSelect({ id: f?.properties?.id ?? f?.id, properties: f.properties, geometry: f.geometry })" [class.active]="selected?.id === (f?.properties?.id ?? f?.id)">
|
@for (f of unlocatedOrOnline; track f.id) {
|
||||||
|
<li class="event-item"
|
||||||
|
(click)="onSelect({ id: f?.properties?.id ?? f?.id, properties: f.properties, geometry: f.geometry })"
|
||||||
|
[class.active]="selected?.id === (f?.properties?.id ?? f?.id)">
|
||||||
<span class="event-icon">📌</span>
|
<span class="event-icon">📌</span>
|
||||||
<div class="event-meta">
|
<div class="event-meta">
|
||||||
<div class="event-title">{{f?.properties?.label || f?.properties?.name || 'Événement'}}</div>
|
<div class="event-title">{{f?.properties?.label || f?.properties?.name || 'Événement'}}</div>
|
||||||
<div class="event-when">{{f?.properties?.start || f?.properties?.when || '—'}}</div>
|
<div class="event-when">{{f?.properties?.start || f?.properties?.when || '—'}}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="agenda-main">
|
<main class="agenda-main">
|
||||||
@if (selected) {
|
@if (selected) {
|
||||||
<div class="event-edit-panel">
|
<div class="event-edit-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>Détails</h3>
|
<h3>Détails</h3>
|
||||||
<button class="btn-close" (click)="selected = null">×</button>
|
<button class="btn-close" (click)="selected = null">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
<app-edit-form
|
<app-edit-form [selected]="selected" (saved)="onSaved($event)" (created)="onCreated($event)"
|
||||||
[selected]="selected"
|
|
||||||
(saved)="onSaved($event)"
|
|
||||||
(created)="onCreated($event)"
|
|
||||||
(deleted)="onDeleted($event)">
|
(deleted)="onDeleted($event)">
|
||||||
</app-edit-form>
|
</app-edit-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="hint">Sélectionnez un évènement à gauche pour voir les détails.</div>
|
<div class="hint">Sélectionnez un évènement à gauche pour voir les détails.</div>
|
||||||
}
|
}
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
} @else {
|
||||||
} @else {
|
<div class="table-wrapper" style="overflow:auto;height:100%;">
|
||||||
<div class="table-wrapper" style="overflow:auto;height:100%;">
|
<table style="width:100%;border-collapse:collapse;">
|
||||||
<table style="width:100%;border-collapse:collapse;">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Type</th>
|
||||||
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Type</th>
|
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Label</th>
|
||||||
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Label</th>
|
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Start</th>
|
||||||
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Start</th>
|
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Stop</th>
|
||||||
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Stop</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
@for (f of filteredFeatures; track f.id) {
|
||||||
@for (f of filteredFeatures; track f.id) {
|
<tr (click)="onSelect({ id: f?.properties?.id ?? f?.id, properties: f.properties, geometry: f.geometry })"
|
||||||
<tr (click)="onSelect({ id: f?.properties?.id ?? f?.id, properties: f.properties, geometry: f.geometry })" style="cursor:pointer;">
|
style="cursor:pointer;">
|
||||||
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.what}}</td>
|
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.what}}</td>
|
||||||
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.label || f?.properties?.name}}</td>
|
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.label || f?.properties?.name}}
|
||||||
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.start || f?.properties?.when}}</td>
|
</td>
|
||||||
|
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.start || f?.properties?.when}}
|
||||||
|
</td>
|
||||||
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.stop}}</td>
|
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.stop}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (selectedIds.length) {
|
<!-- Boutons flottants en bas à droite -->
|
||||||
<div class="batch-panel">
|
<div class="floating-actions">
|
||||||
<div class="panel">
|
<button class="fab counter" (click)="toggleUnlocatedPanel()" title="{{unlocatedOrOnline.length}} évènements non localisés ou en ligne">
|
||||||
<div class="row">
|
{{unlocatedOrOnline.length}}
|
||||||
<label>Action de masse</label>
|
</button>
|
||||||
<select class="input" [(ngModel)]="batchAction">
|
<button class="fab plus" (click)="createMammoth()" title="Créer un nouvel évènement (mammouth)">+
|
||||||
<option value="none">Choisir...</option>
|
</button>
|
||||||
<option value="changeWhat">Changer le type d'évènement (what)</option>
|
</div>
|
||||||
<option value="setField">Remplacer une propriété</option>
|
|
||||||
<option value="delete">Supprimer</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
@if (batchAction==='changeWhat') {
|
|
||||||
<div class="row">
|
|
||||||
<label>Nouveau "what"</label>
|
|
||||||
<input class="input" type="text" [(ngModel)]="batchWhat" placeholder="ex: traffic.roadwork" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (batchAction==='setField') {
|
|
||||||
<div class="row">
|
|
||||||
<label>Clé de propriété</label>
|
|
||||||
<input class="input" type="text" [(ngModel)]="batchFieldKey" placeholder="ex: where" />
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>Nouvelle valeur</label>
|
|
||||||
<input class="input" type="text" [(ngModel)]="batchFieldValue" placeholder="ex: Paris, 12e" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="actions">
|
|
||||||
<button class="btn" (click)="applyBatch()" [disabled]="batchAction==='none'">Appliquer</button>
|
|
||||||
<button class="btn btn-ghost" (click)="clearSelection()">Annuler</button>
|
|
||||||
</div>
|
|
||||||
@if (batchSummary) {
|
|
||||||
<div class="summary">
|
|
||||||
<span>Succès: {{batchSummary.success}}</span>
|
|
||||||
<span>Échecs: {{batchSummary.failed}}</span>
|
|
||||||
<span>Erreurs réseau: {{batchSummary.networkErrors}}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Boutons flottants en bas à droite -->
|
|
||||||
<div class="floating-actions">
|
|
||||||
<button class="fab counter" (click)="toggleUnlocatedPanel()" title="Non localisés ou en ligne">
|
|
||||||
{{unlocatedOrOnline.length}}
|
|
||||||
</button>
|
|
||||||
<button class="fab plus" (click)="createMammoth()" title="Créer un nouvel évènement (mammouth)">+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
.hint{ color: #64748b; padding: 20px; }
|
.hint{ color: #64748b; padding: 20px; }
|
||||||
.floating-actions{
|
.floating-actions{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 16px;
|
right: 1rem;
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -261,4 +261,32 @@ app-edit-form{
|
||||||
right: 5rem;
|
right: 5rem;
|
||||||
bottom: 1.1rem;
|
bottom: 1.1rem;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
table{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
td:nth-of-type(1){
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
th, td{
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
tr:nth-child(even){
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
tr:nth-child(odd){
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
tr:hover{
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
th{
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ export class Home implements OnInit, OnDestroy {
|
||||||
showTable = false;
|
showTable = false;
|
||||||
showFilters = false;
|
showFilters = false;
|
||||||
showEditForm = false;
|
showEditForm = false;
|
||||||
showOptions = false;
|
showOptions = true;
|
||||||
pleinAirMode = false;
|
pleinAirMode = false;
|
||||||
toasts: Array<{ id: number, type: 'success' | 'error' | 'info', message: string }> = [];
|
toasts: Array<{ id: number, type: 'success' | 'error' | 'info', message: string }> = [];
|
||||||
|
|
||||||
|
|
@ -71,6 +71,14 @@ export class Home implements OnInit, OnDestroy {
|
||||||
// Option bbox
|
// Option bbox
|
||||||
useBboxFilter = true;
|
useBboxFilter = true;
|
||||||
currentBbox: { minLng: number, minLat: number, maxLng: number, maxLat: number } | null = null;
|
currentBbox: { minLng: number, minLat: number, maxLng: number, maxLat: number } | null = null;
|
||||||
|
|
||||||
|
// Bbox par défaut pour l'Île-de-France
|
||||||
|
private readonly IDF_BBOX = {
|
||||||
|
minLng: 1.4,
|
||||||
|
minLat: 48.1,
|
||||||
|
maxLng: 3.6,
|
||||||
|
maxLat: 49.2
|
||||||
|
};
|
||||||
// Debounce pour la recherche
|
// Debounce pour la recherche
|
||||||
private searchDebounceTimer: any = null;
|
private searchDebounceTimer: any = null;
|
||||||
// Non localisés / en ligne
|
// Non localisés / en ligne
|
||||||
|
|
@ -84,9 +92,13 @@ export class Home implements OnInit, OnDestroy {
|
||||||
const d = e?.detail || {}; this.pushToast(d.type || 'info', d.message || '');
|
const d = e?.detail || {}; this.pushToast(d.type || 'info', d.message || '');
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// Initialiser la bbox par défaut pour l'Île-de-France
|
||||||
|
this.currentBbox = { ...this.IDF_BBOX };
|
||||||
|
|
||||||
this.route.queryParamMap.subscribe(map => {
|
this.route.queryParamMap.subscribe(map => {
|
||||||
const id = (map.get('id') || '').trim();
|
const id = (map.get('id') || '').trim();
|
||||||
const what = (map.get('what') || '').trim();
|
const what = (map.get('what') || 'culture').trim();
|
||||||
const pleinAir = (map.get('pleinair') || '').trim().toLowerCase();
|
const pleinAir = (map.get('pleinair') || '').trim().toLowerCase();
|
||||||
const preset = (map.get('preset') || '').trim().toLowerCase();
|
const preset = (map.get('preset') || '').trim().toLowerCase();
|
||||||
const limitParam = map.get('limit');
|
const limitParam = map.get('limit');
|
||||||
|
|
@ -111,6 +123,8 @@ export class Home implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.startAutoReload();
|
this.startAutoReload();
|
||||||
|
|
||||||
|
this.loadEvents({ what: "culture" , limit: 100 });
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|
@ -365,7 +379,7 @@ export class Home implements OnInit, OnDestroy {
|
||||||
this.selected = null;
|
this.selected = null;
|
||||||
// Après création rapide en plein air: recharger uniquement ce type pour feedback instantané
|
// Après création rapide en plein air: recharger uniquement ce type pour feedback instantané
|
||||||
this.selectedWhatFilter = w;
|
this.selectedWhatFilter = w;
|
||||||
this.loadEvents({ what: w });
|
this.loadEvents({ what: 'traffic' });
|
||||||
},
|
},
|
||||||
error: () => { this.pushToast('error', 'Échec de création'); }
|
error: () => { this.pushToast('error', 'Échec de création'); }
|
||||||
});
|
});
|
||||||
|
|
@ -616,4 +630,9 @@ export class Home implements OnInit, OnDestroy {
|
||||||
this.loadEvents();
|
this.loadEvents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Méthode pour recharger les événements quand la carte bouge
|
||||||
|
onMapMove(bbox: { minLng: number, minLat: number, maxLng: number, maxLat: number }) {
|
||||||
|
this.setCurrentBbox(bbox);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,12 @@ label { font-size: 0.85rem; color: $color-muted; }
|
||||||
border: 1px solid rgba(0,0,0,0.08);
|
border: 1px solid rgba(0,0,0,0.08);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
|
width: 100%;
|
||||||
|
table{
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.actions{
|
.actions{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue