-
+
-
Presets
+
+ Presets
+ ({{filteredPresetCount()}})
+
@for (g of filteredGroups(); track g.category) {
}
+
+
+ @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{