add search filter, toggle options, count unlocated events
This commit is contained in:
parent
ee48a3c665
commit
d22dbde2e7
12 changed files with 797 additions and 165 deletions
|
|
@ -1,42 +1,46 @@
|
|||
<div class="layout">
|
||||
@if(showOptions){
|
||||
<div class="aside">
|
||||
<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>
|
||||
<div class="aside-content">
|
||||
<div class="toolbar">
|
||||
<!-- <span class="loading-indicator">⏳</span> -->
|
||||
@if (isLoading) {
|
||||
<span class="loading-indicator">⏳</span>
|
||||
}
|
||||
</label>
|
||||
|
||||
<div class="filters-group">
|
||||
@if (showFilters) {
|
||||
<span class="muted">{{filteredFeatures.length}} évènements chargés</span>
|
||||
<hr>
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>Jours à venir</label>
|
||||
<input
|
||||
type="number"
|
||||
class="input"
|
||||
[(ngModel)]="daysAhead"
|
||||
(ngModelChange)="onDaysAheadChange()"
|
||||
min="1"
|
||||
max="30"
|
||||
placeholder="7">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="filters">
|
||||
|
||||
<label (click)="showFilters = !showFilters">
|
||||
Filtre rapide
|
||||
@if (showFilters) {
|
||||
<span>▼</span>
|
||||
} @else {
|
||||
<span>▶</span>
|
||||
}
|
||||
</label>
|
||||
|
||||
<div class="filters-group">
|
||||
@if (showFilters) {
|
||||
<span class="muted">{{filteredFeatures.length}} évènements chargés</span>
|
||||
<hr>
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>Jours à venir</label>
|
||||
<input
|
||||
type="number"
|
||||
class="input"
|
||||
[(ngModel)]="daysAhead"
|
||||
(ngModelChange)="onDaysAheadChange()"
|
||||
min="1"
|
||||
max="30"
|
||||
placeholder="7">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>
|
||||
<input
|
||||
|
|
@ -46,102 +50,130 @@
|
|||
Rechargement auto (1min)
|
||||
</label>
|
||||
</div>
|
||||
</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">
|
||||
<app-what-filter [label]="'Filtrer par type d\'événement'" [available]="availableWhatTypes" [selected]="selectedWhatFilter" (selectedChange)="selectedWhatFilter = $event; onWhatFilterChange()"></app-what-filter>
|
||||
<label>Période (début / fin)</label>
|
||||
<div style="display:flex; gap:6px;">
|
||||
<input class="input" type="date" [(ngModel)]="startDateStr">
|
||||
<input class="input" type="date" [(ngModel)]="endDateStr">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<app-osm></app-osm>
|
||||
|
||||
<app-menu></app-menu>
|
||||
<hr>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <app-unlocated-events [events]="filteredFeatures"></app-unlocated-events> -->
|
||||
|
||||
<hr>
|
||||
@if(showEditForm){
|
||||
|
||||
<div class="guide">
|
||||
<h3>Guide</h3>
|
||||
<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>
|
||||
<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 class="control-group">
|
||||
<button class="btn" (click)="onQuickSearchSubmit()">Rechercher</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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<app-osm></app-osm>
|
||||
|
||||
<app-menu></app-menu>
|
||||
<hr>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <app-unlocated-events [events]="filteredFeatures"></app-unlocated-events> -->
|
||||
|
||||
<hr>
|
||||
@if(showEditForm){
|
||||
|
||||
<div class="guide">
|
||||
<h3>Guide</h3>
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
</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}}
|
||||
|
||||
@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 class="main">
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
<div class="main {{showOptions? 'is-small' : 'is-full'}}">
|
||||
<button class="button toggle-options" (click)="showOptions = !showOptions">
|
||||
Options
|
||||
</button>
|
||||
@if (theme()) {
|
||||
<div class="subtheme-bar">
|
||||
<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>
|
||||
|
|
@ -155,10 +187,54 @@ lastupdate:
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!showTable) {
|
||||
@if (!showTable && !showUnlocatedList) {
|
||||
<div class="map">
|
||||
<app-all-events [features]="filteredFeatures" [selected]="selected" [selectMode]="selectionMode" (selection)="onSelection($event)" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"></app-all-events>
|
||||
</div>
|
||||
} @else if (showUnlocatedList) {
|
||||
<div class="table-wrapper" style="overflow:auto;height:100%;">
|
||||
<div class="unlocated-layout">
|
||||
<aside class="agenda-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3>Sans lieu / en ligne</h3>
|
||||
<small>{{unlocatedOrOnline.length}} évènements</small>
|
||||
</div>
|
||||
<div class="day-groups">
|
||||
<ul class="event-list">
|
||||
@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>
|
||||
<div class="event-meta">
|
||||
<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>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="agenda-main">
|
||||
@if (selected) {
|
||||
<div class="event-edit-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Détails</h3>
|
||||
<button class="btn-close" (click)="selected = null">×</button>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<app-edit-form
|
||||
[selected]="selected"
|
||||
(saved)="onSaved($event)"
|
||||
(created)="onCreated($event)"
|
||||
(deleted)="onDeleted($event)">
|
||||
</app-edit-form>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="hint">Sélectionnez un évènement à gauche pour voir les détails.</div>
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="table-wrapper" style="overflow:auto;height:100%;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
|
|
@ -194,6 +270,7 @@ lastupdate:
|
|||
<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>
|
||||
|
|
@ -203,10 +280,36 @@ lastupdate:
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@
|
|||
grid-template-columns: 400px 1fr;
|
||||
grid-template-rows: 100vh;
|
||||
gap: 0;
|
||||
&.is-small{
|
||||
grid-template-columns: 100px 1fr;
|
||||
}
|
||||
/* Quand la zone principale est en plein écran */
|
||||
&:has(.main.is-full){
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.aside {
|
||||
|
|
@ -32,6 +39,10 @@
|
|||
padding: 16px;
|
||||
padding-bottom: 150px;
|
||||
overflow: auto;
|
||||
/* Masquer l'aside en mode plein écran */
|
||||
.layout:has(.main.is-full) &{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
|
|
@ -39,6 +50,9 @@
|
|||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
&.is-full{
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
|
|
@ -115,6 +129,58 @@
|
|||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
.unlocated-layout{
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
height: 100%;
|
||||
}
|
||||
.agenda-sidebar{
|
||||
overflow: auto;
|
||||
border-right: 1px solid #eee;
|
||||
padding: 10px;
|
||||
}
|
||||
.event-list{ list-style: none; padding: 0; margin: 0; }
|
||||
.event-item{ display: flex; gap: 8px; padding: 8px; cursor: pointer; }
|
||||
.event-item.active{ background: #f0f9ff; }
|
||||
.event-title{ font-weight: 600; }
|
||||
.event-when{ font-size: 12px; color: #64748b; }
|
||||
.agenda-main{ padding: 10px; }
|
||||
.event-edit-panel{ border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; }
|
||||
.panel-header{ display:flex; justify-content: space-between; align-items:center; padding: 8px 12px; border-bottom: 1px solid #e2e8f0; }
|
||||
.panel-content{ padding: 8px; }
|
||||
.btn-close{ background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; width: 32px; height: 32px; cursor: pointer; }
|
||||
.hint{ color: #64748b; padding: 20px; }
|
||||
.floating-actions{
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
z-index: 2000;
|
||||
|
||||
.fab{
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
}
|
||||
.fab.counter{
|
||||
background: #0d9488;
|
||||
}
|
||||
.fab.plus{
|
||||
background: #1976d2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.presets{
|
||||
|
|
@ -125,6 +191,10 @@
|
|||
max-height: 80vh;
|
||||
display: block;
|
||||
|
||||
/* En plein écran, plus d'offset gauche */
|
||||
.layout:has(.main.is-full) &{
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
app-edit-form{
|
||||
position: fixed;
|
||||
|
|
@ -142,6 +212,10 @@ app-edit-form{
|
|||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
z-index: 1000;
|
||||
padding-bottom: 150px;
|
||||
/* En plein écran, plus d'offset gauche */
|
||||
.layout:has(.main.is-full) &{
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subtheme-bar {
|
||||
|
|
@ -155,3 +229,25 @@ app-edit-form{
|
|||
.chip.active { background: #e3f2fd; border-color: #90caf9; }
|
||||
.emoji { margin-right: 6px; }
|
||||
}
|
||||
|
||||
.toggle-options{
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 100;
|
||||
background: white;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
&:hover{
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
&:active{
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
&:focus{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -39,33 +39,68 @@ export class Home implements OnInit, OnDestroy {
|
|||
selected: any | null = null;
|
||||
showTable = false;
|
||||
showFilters = false;
|
||||
showEditForm = true;
|
||||
showEditForm = false;
|
||||
showOptions = false;
|
||||
|
||||
selectionMode: 'none' | 'rectangle' | 'polygon' = 'none';
|
||||
selectedIds: Array<string | number> = [];
|
||||
batchAction: 'none' | 'changeWhat' | 'delete' = 'none';
|
||||
batchAction: 'none' | 'changeWhat' | 'setField' | 'delete' = 'none';
|
||||
batchWhat = '';
|
||||
batchFieldKey = '';
|
||||
batchFieldValue: any = '';
|
||||
batchSummary: { success: number; failed: number; networkErrors: number } | null = null;
|
||||
|
||||
// Nouvelles propriétés pour le rechargement automatique et la sélection de jours
|
||||
autoReloadEnabled = true;
|
||||
autoReloadInterval: any = null;
|
||||
daysAhead = 7; // Nombre de jours dans le futur par défaut
|
||||
isLoading = false;
|
||||
// Formulaire de recherche
|
||||
startDateStr: string | null = null;
|
||||
endDateStr: string | null = null;
|
||||
|
||||
// Propriétés pour les filtres
|
||||
searchText = '';
|
||||
selectedWhatFilter = '';
|
||||
selectedWhatFilter = 'culture';
|
||||
availableWhatTypes: string[] = [];
|
||||
theme = signal<string | null>(null);
|
||||
subthemes: Array<{ key: string, label: string, emoji: string }> = [];
|
||||
activeSubtheme = signal<string | null>(null);
|
||||
// Option bbox
|
||||
useBboxFilter = false;
|
||||
currentBbox: { minLng: number, minLat: number, maxLng: number, maxLat: number } | null = null;
|
||||
// Debounce pour la recherche
|
||||
private searchDebounceTimer: any = null;
|
||||
// Non localisés / en ligne
|
||||
unlocatedOrOnline: Array<any> = [];
|
||||
showUnlocatedList = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.loadEvents();
|
||||
this.route.queryParamMap.subscribe(map => {
|
||||
const id = (map.get('id') || '').trim();
|
||||
const what = (map.get('what') || '').trim();
|
||||
const limitParam = map.get('limit');
|
||||
const limit = limitParam ? Number(limitParam) : null;
|
||||
// Charger selon les query params
|
||||
if (id) {
|
||||
this.loadSingleEvent(id);
|
||||
} else {
|
||||
this.loadEvents({ what: what || undefined, limit: limit || undefined });
|
||||
}
|
||||
// Appliquer filtre par what côté client si fourni
|
||||
if (what) {
|
||||
this.selectedWhatFilter = what;
|
||||
}
|
||||
});
|
||||
this.startAutoReload();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.stopAutoReload();
|
||||
// Nettoyer le timer de debounce
|
||||
if (this.searchDebounceTimer) {
|
||||
clearTimeout(this.searchDebounceTimer);
|
||||
}
|
||||
}
|
||||
|
||||
createEvent() {
|
||||
|
|
@ -76,26 +111,61 @@ export class Home implements OnInit, OnDestroy {
|
|||
|
||||
}
|
||||
|
||||
loadEvents() {
|
||||
loadEvents(overrides: { what?: string; limit?: number; start?: string; end?: string; daysAhead?: number } = {}) {
|
||||
this.isLoading = true;
|
||||
const today = new Date();
|
||||
const endDate = new Date(today);
|
||||
endDate.setDate(today.getDate() + this.daysAhead);
|
||||
const startIso = overrides.start || this.startDateStr || today.toISOString().split('T')[0];
|
||||
let endIso = overrides.end || this.endDateStr || '';
|
||||
if (!endIso) {
|
||||
const d = new Date(today);
|
||||
const span = overrides.daysAhead ?? this.daysAhead;
|
||||
d.setDate(today.getDate() + span);
|
||||
endIso = d.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
const params = {
|
||||
start: today.toISOString().split('T')[0],
|
||||
end: endDate.toISOString().split('T')[0],
|
||||
limit: 3000
|
||||
const params: any = {
|
||||
start: startIso,
|
||||
end: endIso,
|
||||
limit: overrides.limit ?? 10000
|
||||
};
|
||||
if (overrides.what) {
|
||||
params.what = overrides.what;
|
||||
} else if (this.selectedWhatFilter) {
|
||||
params.what = this.selectedWhatFilter;
|
||||
}
|
||||
|
||||
// Ajouter bbox si activé et disponible
|
||||
if (this.useBboxFilter && this.currentBbox) {
|
||||
params.bbox = `${this.currentBbox.minLng},${this.currentBbox.minLat},${this.currentBbox.maxLng},${this.currentBbox.maxLat}`;
|
||||
}
|
||||
|
||||
this.OedbApi.getEvents(params).subscribe((events: any) => {
|
||||
this.features = Array.isArray(events?.features) ? events.features : [];
|
||||
this.computeUnlocatedOrOnline();
|
||||
this.updateAvailableWhatTypes();
|
||||
this.applyFilters();
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadSingleEvent(id: string | number) {
|
||||
this.isLoading = true;
|
||||
this.OedbApi.getEventById(id).subscribe({
|
||||
next: (feature: any) => {
|
||||
const f = (feature && (feature as any).type === 'Feature') ? feature : (feature?.feature || null);
|
||||
this.features = f ? [f] : [];
|
||||
this.filteredFeatures = this.features;
|
||||
this.updateAvailableWhatTypes();
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.features = [];
|
||||
this.filteredFeatures = [];
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startAutoReload() {
|
||||
if (this.autoReloadEnabled && !this.autoReloadInterval) {
|
||||
this.autoReloadInterval = setInterval(() => {
|
||||
|
|
@ -121,7 +191,7 @@ export class Home implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
onDaysAheadChange() {
|
||||
this.loadEvents();
|
||||
this.loadEvents({ daysAhead: this.daysAhead, what: this.selectedWhatFilter || undefined });
|
||||
}
|
||||
|
||||
updateAvailableWhatTypes() {
|
||||
|
|
@ -141,7 +211,15 @@ export class Home implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
onSearchChange() {
|
||||
this.applyFilters();
|
||||
// Annuler le timer précédent s'il existe
|
||||
if (this.searchDebounceTimer) {
|
||||
clearTimeout(this.searchDebounceTimer);
|
||||
}
|
||||
|
||||
// Créer un nouveau timer de 500ms
|
||||
this.searchDebounceTimer = setTimeout(() => {
|
||||
this.applyFilters();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
onWhatFilterChange() {
|
||||
|
|
@ -189,6 +267,8 @@ export class Home implements OnInit, OnDestroy {
|
|||
...this.selected,
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] }
|
||||
};
|
||||
this.showOptions = true;
|
||||
|
||||
} else {
|
||||
const osmUsername = this.osmAuth.getUsername();
|
||||
const whatKey = this.activeSubtheme();
|
||||
|
|
@ -246,34 +326,51 @@ export class Home implements OnInit, OnDestroy {
|
|||
this.selectedIds = [];
|
||||
this.batchAction = 'none';
|
||||
this.batchWhat = '';
|
||||
this.batchFieldKey = '';
|
||||
this.batchFieldValue = '';
|
||||
this.batchSummary = null;
|
||||
}
|
||||
|
||||
async applyBatch() {
|
||||
if (!this.selectedIds.length || this.batchAction === 'none') return;
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
let networkErrors = 0;
|
||||
|
||||
const doUpdate = async (id: string | number, updater: (f: any) => any) => {
|
||||
const feature = this.features.find(f => (f?.properties?.id ?? f?.id) === id);
|
||||
if (!feature) { failed++; return; }
|
||||
const updated = updater(feature);
|
||||
await new Promise<void>((resolve) => {
|
||||
this.OedbApi.updateEvent(id, updated).subscribe({
|
||||
next: () => { success++; resolve(); },
|
||||
error: (err) => { (err?.status === 0 ? networkErrors++ : failed++); resolve(); }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (this.batchAction === 'delete') {
|
||||
for (const id of this.selectedIds) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.OedbApi.deleteEvent(id).subscribe({ next: () => resolve(), error: () => resolve() });
|
||||
this.OedbApi.deleteEvent(id).subscribe({ next: () => { success++; resolve(); }, error: (err) => { (err?.status === 0 ? networkErrors++ : failed++); resolve(); } });
|
||||
});
|
||||
}
|
||||
this.loadEvents();
|
||||
this.clearSelection();
|
||||
return;
|
||||
}
|
||||
if (this.batchAction === 'changeWhat') {
|
||||
} else if (this.batchAction === 'changeWhat') {
|
||||
const what = this.batchWhat.trim();
|
||||
if (!what) return;
|
||||
for (const id of this.selectedIds) {
|
||||
const feature = this.features.find(f => (f?.properties?.id ?? f?.id) === id);
|
||||
if (!feature) continue;
|
||||
const updated = { ...feature, properties: { ...feature.properties, what } };
|
||||
await new Promise<void>((resolve) => {
|
||||
this.OedbApi.updateEvent(id, updated).subscribe({ next: () => resolve(), error: () => resolve() });
|
||||
});
|
||||
await doUpdate(id, (feature: any) => ({ ...feature, properties: { ...feature.properties, what } }));
|
||||
}
|
||||
} else if (this.batchAction === 'setField') {
|
||||
const key = this.batchFieldKey.trim();
|
||||
if (!key) return;
|
||||
for (const id of this.selectedIds) {
|
||||
await doUpdate(id, (feature: any) => ({ ...feature, properties: { ...feature.properties, [key]: this.batchFieldValue } }));
|
||||
}
|
||||
this.loadEvents();
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
this.batchSummary = { success, failed, networkErrors };
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
onCanceled() {
|
||||
|
|
@ -288,6 +385,46 @@ export class Home implements OnInit, OnDestroy {
|
|||
this.showTable = !this.showTable;
|
||||
}
|
||||
|
||||
private isNonLocated(feature: any): boolean {
|
||||
const geom = feature?.geometry;
|
||||
if (!geom || geom.type !== 'Point') return true;
|
||||
const coords = geom.coordinates;
|
||||
if (!Array.isArray(coords) || coords.length !== 2) return true;
|
||||
const [lon, lat] = coords;
|
||||
if (lon == null || lat == null) return true;
|
||||
if (lon === 0 && lat === 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private isOnline(feature: any): boolean {
|
||||
const v = feature?.properties?.online;
|
||||
return v === 'yes' || v === true;
|
||||
}
|
||||
|
||||
private computeUnlocatedOrOnline() {
|
||||
this.unlocatedOrOnline = (this.features || []).filter(f => this.isNonLocated(f) || this.isOnline(f));
|
||||
}
|
||||
|
||||
toggleUnlocatedPanel() {
|
||||
this.showUnlocatedList = !this.showUnlocatedList;
|
||||
}
|
||||
|
||||
createMammoth() {
|
||||
const osmUsername = this.osmAuth.getUsername();
|
||||
this.selected = {
|
||||
id: null,
|
||||
properties: {
|
||||
label: '',
|
||||
description: '',
|
||||
what: 'traffic.mammoth',
|
||||
where: '',
|
||||
...(osmUsername && { last_modified_by: osmUsername })
|
||||
},
|
||||
geometry: { type: 'Point', coordinates: [0, 0] }
|
||||
};
|
||||
this.showEditForm = true;
|
||||
}
|
||||
|
||||
private buildSubthemes() {
|
||||
const t = this.theme();
|
||||
if (!t) { this.subthemes = []; this.activeSubtheme.set(null); return; }
|
||||
|
|
@ -337,4 +474,34 @@ export class Home implements OnInit, OnDestroy {
|
|||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
}
|
||||
|
||||
onQuickSearchSubmit() {
|
||||
const start = (this.startDateStr || '').trim() || undefined;
|
||||
const end = (this.endDateStr || '').trim() || undefined;
|
||||
const days = this.daysAhead;
|
||||
const what = (this.selectedWhatFilter || '').trim() || undefined;
|
||||
this.loadEvents({ start, end, daysAhead: days, what });
|
||||
}
|
||||
|
||||
onBboxFilterToggle() {
|
||||
this.useBboxFilter = !this.useBboxFilter;
|
||||
if (this.useBboxFilter) {
|
||||
// Demander la bbox actuelle à la carte
|
||||
this.requestCurrentBbox();
|
||||
}
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
requestCurrentBbox() {
|
||||
// Cette méthode sera appelée par le composant de carte
|
||||
// pour obtenir la bbox actuelle
|
||||
console.log('Demande de bbox actuelle...');
|
||||
}
|
||||
|
||||
setCurrentBbox(bbox: { minLng: number, minLat: number, maxLng: number, maxLat: number }) {
|
||||
this.currentBbox = bbox;
|
||||
if (this.useBboxFilter) {
|
||||
this.loadEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue