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>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-15 20:40:40 +02:00
1 parent 56c4f38ef0
commit 1d7497bead
11 files changed
+343 -113

No files matched your search

+96 -51
View File
@@ -1,63 +1,108 @@
// Reiseroute für die Kilometer-Statistik, aus den eingetragenen Besuchen (Luftlinie):
// - Reise: Wohnort → Ziel → Wohnort. Beginnt die nächste Reise höchstens einen Tag nach dem Ende der vorigen,
// ist es eine Rundreise (Wohnort → A → B → Wohnort).
// - Ausflug: Besuch, der während eines längeren Aufenthalts beginnt (z. B. David während San José) –
// zählt als Hin- und Rückweg zum Aufenthaltsort.
// Reiseroute für die Kilometer-Statistik, aus Besuchen und Wohnorten (Luftlinie):
// - Reise: Wohnort → Ziel(e) → Wohnort. Welcher Wohnort gilt, entscheidet das "seit"-Datum der Wohnorte.
// - Rundreise: Besuche mit Anreise "vom vorherigen Ort" (oder automatisch: höchstens 1 Tag Abstand)
// gehören zur selben Reise: Wohnort → A → B → Wohnort.
// - Ausflug: Besuch, der während eines längeren Aufenthalts beginnt – Hin- und Rückweg zum Aufenthaltsort.
// - Umzug: Wechsel des Wohnorts (auch ein längerer Auslandsaufenthalt) zählt als eigene Strecke.
// Ohne Wohnort fehlen Hin- und Heimwege; die Reisen werden dann direkt nacheinander verbunden.
import { distanceKm } from './laender.js';
const day = (d) => Math.round(Date.parse(`${d}T12:00:00Z`) / 864e5);
const nextDay = (d) => new Date(Date.parse(`${d}T12:00:00Z`) + 864e5).toISOString().slice(0, 10);
const samePlace = (a, b) => Boolean(a && b && a.lat === b.lat && a.lon === b.lon);
export function travelRoute(visits, cities, home) {
const byId = new Map(cities.map((c) => [c.id, c]));
const items = visits
.map((v) => ({ ...v, city: byId.get(v.cityId), to: v.dateTo || v.dateFrom }))
.filter((v) => v.city)
.sort((a, b) => a.dateFrom.localeCompare(b.dateFrom) || b.to.localeCompare(a.to));
// Ausflüge ihrem Aufenthalt zuordnen
const trips = [];
for (const v of items) {
const base = trips.at(-1)?.visit;
if (base && v.dateFrom >= base.dateFrom && v.dateFrom <= base.to) trips.at(-1).excursions.push(v);
else trips.push({ visit: v, excursions: [] });
}
const legs = [];
const leg = (from, to, kind) => {
if (!from || !to || (from.lat === to.lat && from.lon === to.lon)) return;
legs.push({ from: from.name, to: to.name, km: distanceKm(from, to), kind });
};
let pos = null;
trips.forEach((t, i) => {
const chained = i > 0 && day(t.visit.dateFrom) - day(trips[i - 1].visit.to) <= 1;
if (home && !chained) {
leg(pos, home, 'Heimweg');
pos = home;
}
// Ohne Wohnort werden getrennte Reisen direkt verbunden – das ist dann eine Weiterreise, kein Hinweg
leg(pos, t.visit.city, chained || !home ? 'Weiterreise' : 'Hinweg');
pos = t.visit.city;
for (const ex of t.excursions) {
leg(pos, ex.city, 'Ausflug');
leg(ex.city, pos, 'Ausflug zurück');
}
});
if (home && trips.length) leg(pos, home, 'Heimweg');
return {
legs,
km: legs.reduce((s, l) => s + l.km, 0),
trips: trips.length,
excursions: trips.reduce((s, t) => s + t.excursions.length, 0),
};
/** Wohnort an einem Tag; vor dem ersten Datum gilt der früheste. homes: sortiert, "von Anfang an" zuerst. */
export function homeAt(homes, date) {
let found = null;
for (const h of homes) if (!h.since || h.since <= date) found = h;
return found || homes[0] || null;
}
// Reisetage: jeder Kalendertag nur einmal (Ausflüge während eines Aufenthalts zählen nicht doppelt)
export function travelDays(visits) {
// Besuche zu Reisen zusammenfassen (mit Ausflügen je Besuch)
function groupTrips(visits, cities) {
const byId = new Map(cities.map((c) => [c.id, c]));
const items = visits
.map((v) => ({ ...v, city: byId.get(v.cityId), to: v.dateTo || v.dateFrom, excursions: [] }))
.filter((v) => v.city)
.sort((a, b) => a.dateFrom.localeCompare(b.dateFrom) || b.to.localeCompare(a.to));
const trips = [];
for (const v of items) {
const trip = trips.at(-1);
const stay = trip?.visits.findLast((s) => v.dateFrom >= s.dateFrom && v.dateFrom <= s.to);
if (stay) {
stay.excursions.push(v);
continue;
}
const continued = v.continued ?? (trip && day(v.dateFrom) - day(trip.end) <= 1);
if (trip && continued) {
trip.visits.push(v);
if (v.to > trip.end) trip.end = v.to;
} else {
trips.push({ visits: [v], start: v.dateFrom, end: v.to });
}
}
return trips;
}
export function travelRoute(visits, cities, homes = []) {
const trips = groupTrips(visits, cities);
const dated = homes.filter((h) => h.since);
const legs = [];
const leg = (from, to, kind) => {
if (!from || !to || samePlace(from, to)) return;
legs.push({ from: from.name, to: to.name, km: distanceKm(from, to), kind });
};
let home = homes[0] || null; // aktueller Wohnort
let pos = home; // wo man gerade ist
let next = dated.indexOf(home) + 1; // nächster noch nicht verarbeiteter Wohnort-Wechsel
// Wohnort-Wechsel bis zu einem Tag als Umzug verbuchen (nur zwischen den Reisen)
const moveUntil = (date) => {
while (next < dated.length && dated[next].since <= date) {
leg(pos, dated[next], 'Umzug');
pos = home = dated[next];
next += 1;
}
};
let realTrips = 0;
let excursions = 0;
for (const trip of trips) {
moveUntil(trip.start);
if (!trip.visits.every((v) => samePlace(v.city, home))) realTrips += 1;
trip.visits.forEach((v, i) => {
leg(pos, v.city, i === 0 && home ? 'Hinweg' : 'Weiterreise');
pos = v.city;
for (const ex of v.excursions) {
excursions += 1;
leg(pos, ex.city, 'Ausflug');
leg(ex.city, pos, 'Ausflug zurück');
}
});
if (home) {
// Zurück zum Wohnort, der nach der Reise gilt – hat er sich während der Reise geändert, ist es ein Umzug
const after = homeAt(homes, nextDay(trip.end));
leg(pos, after, after === home ? 'Heimweg' : 'Umzug');
while (next < dated.length && dated[next].since <= nextDay(trip.end)) next += 1;
pos = home = after;
}
}
moveUntil('9999-12-31');
return { legs, km: legs.reduce((s, l) => s + l.km, 0), trips: realTrips, excursions };
}
// Reisetage: jeder Kalendertag einmal; Tage am damaligen Wohnort (z. B. Auslandsaufenthalt) zählen nicht
export function travelDays(visits, cities, homes = []) {
const byId = new Map(cities.map((c) => [c.id, c]));
const days = new Set();
for (const v of visits) {
for (let d = day(v.dateFrom); d <= day(v.dateTo || v.dateFrom); d += 1) days.add(d);
const city = byId.get(v.cityId);
if (!city) continue;
const end = v.dateTo || v.dateFrom;
for (let d = v.dateFrom; d <= end; d = nextDay(d)) {
if (!samePlace(city, homeAt(homes, d))) days.add(d);
}
}
return days.size;
}