From 7accbe0b138975ba581f3771eda2658cf806ad58 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Sun, 5 Oct 2025 00:49:12 +0200 Subject: [PATCH] =?UTF-8?q?afficher=20=C3=A9v=C3=A8nement=20s=C3=A9lection?= =?UTF-8?q?n=C3=A9=20dans=20le=20panel=20de=20gauche?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/forms/edit-form/edit-form.html | 38 +++- frontend/src/app/forms/edit-form/edit-form.ts | 164 ++++++++++++++---- frontend/src/app/pages/home/home.html | 58 +++++-- frontend/src/styles.scss | 7 + 4 files changed, 217 insertions(+), 50 deletions(-) diff --git a/frontend/src/app/forms/edit-form/edit-form.html b/frontend/src/app/forms/edit-form/edit-form.html index c5cccac..6a08a89 100644 --- a/frontend/src/app/forms/edit-form/edit-form.html +++ b/frontend/src/app/forms/edit-form/edit-form.html @@ -1,9 +1,13 @@
- +
-

Presets

+

+ Presets + ({{filteredPresetCount()}}) +

@for (g of filteredGroups(); track g.category) {
@@ -48,19 +52,33 @@
@if (entry.spec?.values; as vs) { - @for (v of vs; track v) { - + } } @else { - + }
}
} + + + @if (extraPropertyKeys().length > 0) { +
+
+ @for (key of extraPropertyKeys(); track key) { +
+ + +
+ } +
+
+ }
     {{currentPreset() | json}}
   
@@ -77,6 +95,11 @@
+
+ +
@@ -93,11 +116,12 @@
- + @if (featureId()) { - + } +
@if (status().state !== 'idle') { diff --git a/frontend/src/app/forms/edit-form/edit-form.ts b/frontend/src/app/forms/edit-form/edit-form.ts index b703dea..21b0607 100644 --- a/frontend/src/app/forms/edit-form/edit-form.ts +++ b/frontend/src/app/forms/edit-form/edit-form.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges, computed, effect, signal } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import oedb from '../../../oedb-types'; import { OedbApi } from '../../services/oedb-api'; import { JsonPipe } from '@angular/common'; @@ -32,6 +32,10 @@ export class EditForm implements OnChanges { }); presetValues = signal>({}); + extraPropertyKeys = signal([]); + private readonly defaultFormKeys = new Set([ + 'label','description','what','where','lat','lon','noLocation','wikidata','featureType','type','start','stop' + ]); status = signal<{ state: 'idle' | 'saving' | 'saved' | 'error', message?: string, what?: string }>({ state: 'idle' }); @@ -46,6 +50,7 @@ export class EditForm implements OnChanges { where: [''], lat: ['', Validators.required], lon: ['', Validators.required], + noLocation: [false], wikidata: [''], featureType: ['point'], type: ['unscheduled'], @@ -110,17 +115,22 @@ export class EditForm implements OnChanges { stop: this.toLocalInputValue(p.stop || new Date(new Date().getTime() + 24 * 3600 * 1000)) }, { emitEvent: false }); - // hydrate presetValues from selected properties for known keys + // Ajouter des contrôles pour les propriétés du preset courant const current = this.currentPreset(); - const result: Record = {}; - if (current && current.properties) { - Object.keys(current.properties).forEach(key => { - if (Object.prototype.hasOwnProperty.call(p, key)) { - result[key] = p[key]; - } - }); - } - this.presetValues.set(result); + const presetKeys = new Set( + current && current.properties ? Object.keys(current.properties) : [] + ); + presetKeys.forEach(key => this.ensureControl(key, p[key] ?? current?.properties?.[key]?.default ?? '')); + + // Ajouter des contrôles pour les autres propriétés présentes dans l'événement sélectionné + const extra: string[] = []; + Object.keys(p).forEach((key) => { + if (this.defaultFormKeys.has(key)) return; + if (presetKeys.has(key)) return; + this.ensureControl(key, p[key]); + extra.push(key); + }); + this.extraPropertyKeys.set(extra); } } @@ -134,13 +144,12 @@ export class EditForm implements OnChanges { description: preset.description || this.form.value.description }); - // initialize presetValues with defaults if any - const result: Record = {}; + // Créer/mettre à jour les contrôles dynamiques pour les propriétés du preset const props = preset.properties || {}; Object.keys(props).forEach(k => { - if (Object.prototype.hasOwnProperty.call(props[k], 'default')) result[k] = props[k].default; + const initial = Object.prototype.hasOwnProperty.call(props[k], 'default') ? props[k].default : ''; + this.ensureControl(k, (this.selected?.properties?.[k] ?? initial)); }); - this.presetValues.set(result); // adjust stop based on preset duration const startIso = this.toIsoFromLocalInput(this.form.value.start); @@ -149,6 +158,18 @@ export class EditForm implements OnChanges { const stop = new Date(start.getTime() + preset.durationHours * 3600 * 1000); this.form.patchValue({ stop: this.toLocalInputValue(stop) }, { emitEvent: true }); } + + // Recalculer les extra properties (non définies par le preset) + const presetKeys = new Set(Object.keys(props)); + const currentProps = this.selected?.properties || {}; + const extra: string[] = []; + Object.keys(currentProps).forEach((k) => { + if (this.defaultFormKeys.has(k)) return; + if (presetKeys.has(k)) return; + this.ensureControl(k, currentProps[k]); + extra.push(k); + }); + this.extraPropertyKeys.set(extra); } private groupPresets(query: string): Array<{ category: string, items: Array<{ key: string, label: string, emoji: string }> }> { @@ -177,7 +198,46 @@ export class EditForm implements OnChanges { } onSubmit() { - const val = this.form.value; + const val = this.form.value as any; + + // Validation minimale: what obligatoire; coordonnées lat/lon obligatoires si non en ligne/sans lieu + if (!val.what || String(val.what).trim() === '') { + this.status.set({ state: 'error', what: val.what, message: 'Le champ "what" est requis' }); + setTimeout(() => this.status.set({ state: 'idle' }), 3000); + return; + } + + const isNoLocation = !!val.noLocation; + const numLat = Number(val.lat); + const numLon = Number(val.lon); + const haveCoords = Number.isFinite(numLat) && Number.isFinite(numLon); + if (!isNoLocation && !haveCoords) { + this.status.set({ state: 'error', what: val.what, message: 'Latitude et longitude sont requises' }); + setTimeout(() => this.status.set({ state: 'idle' }), 3000); + return; + } + + // Construire la géométrie selon le mode + let geometry: any; + if (isNoLocation) { + // Polygone englobant le monde (bbox globale) + geometry = { + type: 'Polygon', + coordinates: [[ + [-180, -90], + [-180, 90], + [180, 90], + [180, -90], + [-180, -90] + ]] + }; + } else { + geometry = { + type: 'Point', + coordinates: [Number(val.lon), Number(val.lat)] + }; + } + const feature: any = { type: 'Feature', properties: { @@ -188,32 +248,40 @@ export class EditForm implements OnChanges { wikidata: val.wikidata, type: val.type, start: this.toIsoFromLocalInput(val.start), - stop: this.toIsoFromLocalInput(val.stop) + stop: this.toIsoFromLocalInput(val.stop), + ...(isNoLocation ? { no_location: true } : {}) }, - geometry: { - type: 'Point', - coordinates: [Number(val.lon), Number(val.lat)] - } + geometry }; // Apply default duration from preset when creating a new event - const preset = (oedb.presets.what as any)[val.what]; - if ((!this.featureId()) && preset && typeof preset.durationHours === 'number') { + const durationPreset = (oedb.presets.what as any)[val.what]; + if ((!this.featureId()) && durationPreset && typeof durationPreset.durationHours === 'number') { // already set from form; ensure consistency if empty if (!feature.properties.start || !feature.properties.stop) { const start = new Date(); - const stop = new Date(start.getTime() + preset.durationHours * 3600 * 1000); + const stop = new Date(start.getTime() + durationPreset.durationHours * 3600 * 1000); feature.properties.start = start.toISOString(); feature.properties.stop = stop.toISOString(); } } const id = this.featureId(); - // merge dynamic preset properties - const extra = this.presetValues(); - Object.keys(extra || {}).forEach(k => { - feature.properties[k] = extra[k]; - }); + // Ajouter les propriétés issues du preset (contrôles dynamiques) + const submitPreset = (oedb.presets.what as any)[val.what]; + if (submitPreset && submitPreset.properties) { + Object.keys(submitPreset.properties).forEach((k: string) => { + if (this.form.contains(k)) { + feature.properties[k] = this.form.get(k)?.value; + } + }); + } + // Ajouter les propriétés extra (non définies par le preset) + for (const k of this.extraPropertyKeys()) { + if (this.form.contains(k)) { + feature.properties[k] = this.form.get(k)?.value; + } + } this.status.set({ state: 'saving', what: val.what, message: 'Envoi en cours…' }); @@ -222,6 +290,8 @@ export class EditForm implements OnChanges { next: (res) => { this.status.set({ state: 'saved', what: val.what, message: 'Évènement mis à jour' }); this.saved.emit(res); + // Quitter l'édition après succès + this.canceled.emit(); setTimeout(() => this.status.set({ state: 'idle' }), 3000); }, error: (err) => { @@ -235,6 +305,8 @@ export class EditForm implements OnChanges { next: (res) => { this.status.set({ state: 'saved', what: val.what, message: 'Évènement créé' }); this.created.emit(res); + // Quitter l'édition après succès + this.canceled.emit(); setTimeout(() => this.status.set({ state: 'idle' }), 3000); }, error: (err) => { @@ -247,9 +319,8 @@ export class EditForm implements OnChanges { } onPresetValueChange(key: string, value: any) { - const current = { ...this.presetValues() }; - current[key] = value; - this.presetValues.set(current); + // Conservé pour compat, plus utilisé avec formControlName + if (this.form.contains(key)) this.form.get(key)?.setValue(value); } onDelete() { @@ -270,6 +341,26 @@ export class EditForm implements OnChanges { }); } + onWhatKeyup() { + // Le filtrage se base déjà sur filteredGroups() qui lit form.what, + // donc un keyup déclenche le recalcul via Angular forms. + // On garde cette méthode pour des actions annexes si besoin. + } + + filteredPresetCount() { + try { + const groups = this.filteredGroups(); + return groups.reduce((acc, g) => acc + (g.items?.length || 0), 0); + } catch { + return 0; + } + } + + resetPresetFilter() { + // Réinitialise le champ what pour afficher tous les presets + this.form.patchValue({ what: '' }); + } + onCancelEdit() { this.selected = null; this.featureId.set(null); @@ -312,6 +403,15 @@ export class EditForm implements OnChanges { return date.toISOString(); } + private ensureControl(key: string, initial: any) { + if (!this.form.contains(key)) { + this.form.addControl(key, new FormControl(initial)); + } else { + // Mettre à jour sans émettre pour éviter des boucles + this.form.get(key)?.setValue(initial, { emitEvent: false }); + } + } + private humanDuration(startIso: string, stopIso: string): string { const a = new Date(startIso).getTime(); const b = new Date(stopIso).getTime(); diff --git a/frontend/src/app/pages/home/home.html b/frontend/src/app/pages/home/home.html index a29457c..e9e5426 100644 --- a/frontend/src/app/pages/home/home.html +++ b/frontend/src/app/pages/home/home.html @@ -79,25 +79,61 @@ } - @if(selected !== null){ -
- -

sélectionné:

- -{{selected.properties.label}} -{{selected.properties.name}} -
- }
- +
-
+ + @if(selected !== null){ +
+ +

sélectionné: {{selected.properties.name}}

+ +{{selected.properties.label}} +
+{{selected.properties.label}} +
+{{selected.properties.what}} +
+{{selected.properties.where}} +
+{{selected.properties.lat}} +
+{{selected.properties.lon}} + +
+{{selected.properties.wikidata}} +
+{{selected.properties.featureType}} +
+{{selected.properties.type}} +
+start: +{{selected.properties.start}} +
+end: +{{selected.properties.stop}} +
+source +{{selected.properties.source}} +
+description: +{{selected.properties.description}} +
+createdate: +{{selected.properties.createdate}} +
+lastupdate: +{{selected.properties.lastupdate}} + +
+ } +
@if (!showTable) {
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 10f23b1..4325d29 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -131,6 +131,13 @@ label { font-size: 0.85rem; color: $color-muted; } .aside{ padding-bottom: 150px; + .selected{ + padding: 2rem 1rem; + border-radius: 10px; + border: 1px solid rgba(0,0,0,0.08); + margin-bottom: 10px; + background: #f0f0f0; + } } .actions{