import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges, computed, effect, signal } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import oedb from '../../../oedb-types'; import { OedbApi } from '../../services/oedb-api'; import { JsonPipe } from '@angular/common'; @Component({ selector: 'app-edit-form', standalone: true, imports: [ReactiveFormsModule, JsonPipe], templateUrl: './edit-form.html', styleUrl: './edit-form.scss' }) export class EditForm implements OnChanges { @Input() selected: any | null = null; @Output() saved = new EventEmitter(); @Output() created = new EventEmitter(); @Output() deleted = new EventEmitter(); form: FormGroup; allPresets: Array<{ key: string, label: string, emoji: string, category: string, description?: string, durationHours?: number, properties?: Record }>; filteredGroups = computed(() => this.groupPresets(this.form.get('what')?.value || '')); currentPreset = computed(() => { const key = this.form.get('what')?.value || ''; return (oedb.presets.what as any)[key] || null; }); presetEntries = computed(() => { const p = this.currentPreset(); const props = (p && p.properties) ? p.properties as Record : {}; return Object.keys(props).map(k => ({ key: k, spec: props[k] })); }); presetValues = signal>({}); status = signal<{ state: 'idle' | 'saving' | 'saved' | 'error', message?: string, what?: string }>({ state: 'idle' }); featureId = signal(null); durationHuman = signal(''); constructor(private fb: FormBuilder, private api: OedbApi) { this.form = this.fb.group({ label: ['', Validators.required], description: [''], what: ['', Validators.required], where: [''], lat: ['', Validators.required], lon: ['', Validators.required], wikidata: [''], featureType: ['point'], type: ['unscheduled'], start: [''], stop: [''] }); const what = oedb.presets.what as Record; this.allPresets = Object.keys(what).map(k => ({ key: k, label: what[k].label || k, emoji: what[k].emoji || '', category: what[k].category || 'Autres', description: what[k].description || '' })); // initialize default 24h window const now = new Date(); const in24h = new Date(now.getTime() + 24 * 3600 * 1000); this.form.patchValue({ start: this.toLocalInputValue(now), stop: this.toLocalInputValue(in24h) }, { emitEvent: false }); // initial fill if provided on first render this.fillFormFromSelected(); // watch start/stop changes to update human duration this.form.valueChanges.subscribe(v => { const startIso = this.toIsoFromLocalInput(v.start); const stopIso = this.toIsoFromLocalInput(v.stop); if (startIso && stopIso) this.durationHuman.set(this.humanDuration(startIso, stopIso)); }); } ngOnChanges(changes: SimpleChanges): void { if (changes['selected']) { this.fillFormFromSelected(); } } private fillFormFromSelected() { const sel = this.selected; if (sel && sel.properties) { const propId = sel?.properties?.id ?? sel?.properties?.uuid; this.featureId.set((propId ?? sel.id) ?? null); const p = sel.properties || {}; const coords = sel?.geometry?.coordinates || []; this.form.patchValue({ label: p.label || p.name || '', id: p.id || '', description: p.description || '', what: p.what || '', "what:series": p['what:series'] || '', where: p.where || '', lat: coords[1] ?? '', lon: coords[0] ?? '', wikidata: p.wikidata || '', featureType: 'point', type: p.type || this.form.value.type || 'unscheduled', start: this.toLocalInputValue(p.start || p.when || new Date()), stop: this.toLocalInputValue(p.stop || new Date(new Date().getTime() + 24 * 3600 * 1000)) }, { emitEvent: false }); // hydrate presetValues from selected properties for known keys 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); } } applyPreset(key: string) { const what = oedb.presets.what as Record; const preset = what[key]; if (!preset) return; this.form.patchValue({ what: key, label: preset.label || this.form.value.label, description: preset.description || this.form.value.description }); // initialize presetValues with defaults if any const result: Record = {}; const props = preset.properties || {}; Object.keys(props).forEach(k => { if (Object.prototype.hasOwnProperty.call(props[k], 'default')) result[k] = props[k].default; }); this.presetValues.set(result); // adjust stop based on preset duration const startIso = this.toIsoFromLocalInput(this.form.value.start); if (typeof preset.durationHours === 'number' && startIso) { const start = new Date(startIso); const stop = new Date(start.getTime() + preset.durationHours * 3600 * 1000); this.form.patchValue({ stop: this.toLocalInputValue(stop) }, { emitEvent: true }); } } private groupPresets(query: string): Array<{ category: string, items: Array<{ key: string, label: string, emoji: string }> }> { const q = String(query || '').trim().toLowerCase(); const matches = (p: typeof this.allPresets[number]) => { if (!q) return true; return ( p.key.toLowerCase().includes(q) || (p.label || '').toLowerCase().includes(q) || (p.description || '').toLowerCase().includes(q) || (p.category || '').toLowerCase().includes(q) ); }; const grouped: Record> = {}; for (const p of this.allPresets) { if (!matches(p)) continue; const cat = p.category || 'Autres'; if (!grouped[cat]) grouped[cat] = []; grouped[cat].push({ key: p.key, label: p.label, emoji: p.emoji }); } return Object.keys(grouped) .sort((a, b) => a.localeCompare(b)) .map(cat => ({ category: cat, items: grouped[cat].sort((a, b) => a.label.localeCompare(b.label)) })); } onSubmit() { const val = this.form.value; const feature: any = { type: 'Feature', properties: { label: val.label, description: val.description, what: val.what, where: val.where, wikidata: val.wikidata, type: val.type, start: this.toIsoFromLocalInput(val.start), stop: this.toIsoFromLocalInput(val.stop) }, geometry: { type: 'Point', coordinates: [Number(val.lon), Number(val.lat)] } }; // 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') { // 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); 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]; }); this.status.set({ state: 'saving', what: val.what, message: 'Envoi en cours…' }); if (id !== null && id !== undefined && id !== '') { this.api.updateEvent(id, feature).subscribe({ next: (res) => { this.status.set({ state: 'saved', what: val.what, message: 'Évènement mis à jour' }); this.saved.emit(res); setTimeout(() => this.status.set({ state: 'idle' }), 3000); }, error: (err) => { this.status.set({ state: 'error', what: val.what, message: 'Erreur lors de la mise à jour' }); console.error(err); setTimeout(() => this.status.set({ state: 'idle' }), 3000); } }); } else { this.api.createEvent(feature).subscribe({ next: (res) => { this.status.set({ state: 'saved', what: val.what, message: 'Évènement créé' }); this.created.emit(res); setTimeout(() => this.status.set({ state: 'idle' }), 3000); }, error: (err) => { this.status.set({ state: 'error', what: val.what, message: 'Erreur lors de la création' }); console.error(err); setTimeout(() => this.status.set({ state: 'idle' }), 3000); } }); } } onPresetValueChange(key: string, value: any) { const current = { ...this.presetValues() }; current[key] = value; this.presetValues.set(current); } onDelete() { const id = this.featureId(); if (id === null || id === undefined || id === '') return; this.status.set({ state: 'saving', what: this.form.value.what, message: 'Suppression…' }); this.api.deleteEvent(id).subscribe({ next: (res) => { this.status.set({ state: 'saved', what: this.form.value.what, message: 'Évènement supprimé' }); this.deleted.emit(res); setTimeout(() => this.status.set({ state: 'idle' }), 3000); }, error: (err) => { this.status.set({ state: 'error', what: this.form.value.what, message: 'Erreur lors de la suppression' }); console.error(err); setTimeout(() => this.status.set({ state: 'idle' }), 3000); } }); } onCancelEdit() { this.selected = null; this.featureId.set(null); this.form.reset({ label: '', description: '', what: '', where: '', lat: '', lon: '', wikidata: '', featureType: 'point', type: 'unscheduled', start: this.toLocalInputValue(new Date()), stop: this.toLocalInputValue(new Date(new Date().getTime() + 24 * 3600 * 1000)) }); this.presetValues.set({}); this.status.set({ state: 'idle' }); } private toLocalInputValue(d: string | Date): string { const date = (typeof d === 'string') ? new Date(d) : d; if (Number.isNaN(date.getTime())) return ''; const pad = (n: number) => n.toString().padStart(2, '0'); const y = date.getFullYear(); const m = pad(date.getMonth() + 1); const da = pad(date.getDate()); const h = pad(date.getHours()); const mi = pad(date.getMinutes()); return `${y}-${m}-${da}T${h}:${mi}`; } private toIsoFromLocalInput(s?: string): string | null { if (!s) return null; // Treat input as local time and convert to ISO const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})$/.exec(s); if (!m) return null; const date = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), 0, 0); return date.toISOString(); } private humanDuration(startIso: string, stopIso: string): string { const a = new Date(startIso).getTime(); const b = new Date(stopIso).getTime(); if (!Number.isFinite(a) || !Number.isFinite(b) || b <= a) return ''; const ms = b - a; const hours = Math.floor(ms / 3600000); const days = Math.floor(hours / 24); const h = hours % 24; if (days > 0 && h > 0) return `${days} j ${h} h`; if (days > 0) return `${days} j`; return `${h} h`; } }