Files
p42.f-w/frontend/src/components/reisen/VisitDialog.jsx
T
guyg1474andClaude Opus 5.5 1d7497bead Reisen: Wohnorte mit Datum, Anreise je Besuch, Umzüge in der Kilometer-Rechnung
- Wohnorte als Liste mit "seit"-Datum (Tabelle travel_homes, ersetzt den
  einzelnen Wohnort; Migration Schritt 5 übernimmt ihn): Reisen starten
  beim damals gültigen Wohnort, Wechsel zählen als Umzug, Tage am
  Wohnort nicht als Reisetage
- "In dieser Zeit hier gewohnt" im Besuch: legt Wohnort ab Beginn und den
  vorherigen wieder ab Ende + 1 Tag an
- Anreise je Besuch (automatisch / von zuhause / vom vorherigen Ort):
  Rundreisen auch mit mehreren Tagen zwischen den Orten
- Fix: Besuche bleiben bearbeitbar, auch wenn eine Fotodatei fehlt;
  Speicherfehler erscheinen im Dialog statt verdeckt dahinter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:40:40 +02:00

122 lines
5.6 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, useState } from 'react';
import { imgUrl, uploadImage } from '../../api.js';
/**
* Besuch einer Stadt eintragen oder bearbeiten: Zeitraum, Begleitung, Notizen, Fotos.
* Fotos werden beim Hochladen auf dem Server von Metadaten (GPS usw.) befreit.
* onSave(body) / onDelete() liefern bei Erfolg den neuen Stand (truthy).
*/
// pageError: Fehlermeldung der Seite (z. B. vom Speichern) – im Dialog anzeigen, sonst läge sie verdeckt dahinter
export default function VisitDialog({ ref, city, visit, onSave, onDelete, onLivedThere, busy, pageError }) {
const today = new Date().toISOString().slice(0, 10);
const [form, setForm] = useState(() => ({
dateFrom: visit?.dateFrom || today,
dateTo: visit?.dateTo || '',
companions: visit?.companions || '',
notes: visit?.notes || '',
photos: visit?.photos || [],
// Anreise: '' automatisch, 'home' von zuhause, 'continued' vom vorherigen Ort weitergereist (Rundreise)
arrival: visit?.continued == null ? '' : visit.continued ? 'continued' : 'home',
}));
const [uploading, setUploading] = useState(0);
const [error, setError] = useState(null);
const fileInput = useRef(null);
const field = (name) => ({ value: form[name], onChange: (e) => setForm((f) => ({ ...f, [name]: e.target.value })) });
async function addPhotos(files) {
setError(null);
for (const file of files) {
setUploading((n) => n + 1);
try {
const name = await uploadImage(file);
setForm((f) => ({ ...f, photos: [...f.photos, name] }));
} catch (e) {
setError(`${file.name}: ${e.message}`);
} finally {
setUploading((n) => n - 1);
}
}
}
async function submit(e) {
e.preventDefault();
const { arrival, ...rest } = form;
const continued = arrival === '' ? null : arrival === 'continued';
if (await onSave({ ...rest, dateTo: form.dateTo || null, continued })) ref.current.close();
}
async function remove() {
if (!window.confirm(`Besuch in ${city.name} löschen?`)) return;
if (await onDelete()) ref.current.close();
}
return (
<dialog className="modal task-dialog" ref={ref}>
<form className="form" onSubmit={submit}>
<h2>{visit ? 'Besuch bearbeiten' : 'Besuch eintragen'} – {city.name}</h2>
<div className="task-dialog-row">
<label>
Von
<input type="date" {...field('dateFrom')} required max={today} />
</label>
<label>
Bis
<input type="date" {...field('dateTo')} min={form.dateFrom} aria-describedby="bis-hint" />
</label>
</div>
<p className="hint visit-hint" id="bis-hint">„Bis“ leer lassen für einen einzelnen Tag.</p>
<label>
Anreise
<select {...field('arrival')}>
<option value="">Automatisch (höchstens 1 Tag nach dem vorigen Besuch = Weiterreise)</option>
<option value="home">Von zuhause</option>
<option value="continued">Vom vorherigen Ort weitergereist (Rundreise)</option>
</select>
</label>
<label>
Begleitung <small>(optional)</small>
<input {...field('companions')} maxLength={200} placeholder="z. B. Anna, Ben" />
</label>
<label>
Notizen <small>(optional)</small>
<textarea {...field('notes')} rows={4} maxLength={5000} placeholder="Was war schön? Tipps fürs nächste Mal …" />
</label>
<fieldset className="link-picker">
<legend>Fotos <small>({form.photos.length})</small></legend>
{form.photos.length > 0 && (
<ul className="visit-photos">
{form.photos.map((p) => (
<li key={p}>
<img src={imgUrl(p)} alt="" loading="lazy" />
<button type="button" className="link-btn danger" onClick={() => setForm((f) => ({ ...f, photos: f.photos.filter((x) => x !== p) }))} aria-label="Foto entfernen">✕</button>
</li>
))}
</ul>
)}
<input ref={fileInput} type="file" accept="image/jpeg,image/png,image/webp,image/gif" multiple hidden
onChange={(e) => { addPhotos([...e.target.files]); e.target.value = ''; }} />
<button className="btn ghost" type="button" onClick={() => fileInput.current.click()} disabled={uploading > 0}>
{uploading ? `Lade ${uploading} Foto${uploading === 1 ? '' : 's'} hoch …` : '+ Fotos hinzufügen'}
</button>
<p className="hint">Standort, Kamera und Aufnahmezeit werden beim Hochladen aus den Fotos entfernt.</p>
</fieldset>
{visit?.dateTo && visit.dateTo !== visit.dateFrom && (
<p className="hint visit-lived">
Längerer Aufenthalt, z. B. Auslandssemester?{' '}
<button className="link-btn" type="button" disabled={busy} onClick={async () => { if (await onLivedThere(visit)) ref.current.close(); }}>
⌂ In dieser Zeit hier gewohnt
</button>
{' '}– macht {city.name} für diesen Zeitraum zum Wohnort (Strecken als Umzug, keine Reisetage).
</p>
)}
{(error || pageError) && <p className="error" role="alert">{error || pageError}</p>}
<div className="form-actions">
{visit && <button className="link-btn danger task-delete" type="button" disabled={busy} onClick={remove}>Löschen</button>}
<button className="btn ghost" type="button" onClick={() => ref.current.close()}>Abbrechen</button>
<button className="btn" type="submit" disabled={busy || uploading > 0}>{visit ? 'Speichern' : 'Eintragen'}</button>
</div>
</form>
</dialog>
);
}