feat: Abfrageort bei Gezeiten speichern und anzeigen
Ort oder GPS-Koordinaten werden im Entry-Payload persistiert und im Tiden-Accordion sowie im Live-Journal-Modal als lesbare Zeile angezeigt. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4628,6 +4628,14 @@ html.theme-cupertino .events-scroll-container {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tides-panel__location {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--app-text);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
.tides-panel__fields {
|
.tides-panel__fields {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ const formatSpeedKn = (speedKn: number) =>
|
|||||||
import { parseOwmCurrentWeather } from '../utils/openWeatherMap.js'
|
import { parseOwmCurrentWeather } from '../utils/openWeatherMap.js'
|
||||||
import { fetchOpenWeatherCurrent, WeatherApiError } from '../services/weather.js'
|
import { fetchOpenWeatherCurrent, WeatherApiError } from '../services/weather.js'
|
||||||
import { fetchTidesByPlace, fetchTidesNearby, TidesApiError } from '../services/tides.js'
|
import { fetchTidesByPlace, fetchTidesNearby, TidesApiError } from '../services/tides.js'
|
||||||
import { resolveTideFetchLocation } from '../utils/tideLocation.js'
|
import {
|
||||||
|
buildTideLocationMeta,
|
||||||
|
formatTideLocationLabel,
|
||||||
|
resolveTideFetchLocation
|
||||||
|
} from '../utils/tideLocation.js'
|
||||||
import { parseTideTurtleForDate } from '../utils/tideTurtle.js'
|
import { parseTideTurtleForDate } from '../utils/tideTurtle.js'
|
||||||
import {
|
import {
|
||||||
geolocationErrorI18nKey,
|
geolocationErrorI18nKey,
|
||||||
@@ -211,10 +215,7 @@ export default function LiveLogView({
|
|||||||
const [tidePreview, setTidePreview] = useState<{
|
const [tidePreview, setTidePreview] = useState<{
|
||||||
highWater: string
|
highWater: string
|
||||||
lowWater: string
|
lowWater: string
|
||||||
placeName?: string
|
location: ReturnType<typeof buildTideLocationMeta>
|
||||||
distanceKm?: number
|
|
||||||
source: 'gps' | 'departure'
|
|
||||||
departureQuery?: string
|
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
||||||
const [commentText, setCommentText] = useState('')
|
const [commentText, setCommentText] = useState('')
|
||||||
@@ -851,10 +852,7 @@ export default function LiveLogView({
|
|||||||
setTidePreview({
|
setTidePreview({
|
||||||
highWater: parsed.highWater,
|
highWater: parsed.highWater,
|
||||||
lowWater: parsed.lowWater,
|
lowWater: parsed.lowWater,
|
||||||
placeName: parsed.placeName,
|
location: buildTideLocationMeta(location, data)
|
||||||
distanceKm: parsed.distanceKm,
|
|
||||||
source: location.source,
|
|
||||||
departureQuery: location.mode === 'by-place' ? location.query : undefined
|
|
||||||
})
|
})
|
||||||
setModal('tides')
|
setModal('tides')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -886,7 +884,8 @@ export default function LiveLogView({
|
|||||||
void runQuickAction(async () => {
|
void runQuickAction(async () => {
|
||||||
await patchEntryTides(logbookId, entryId, {
|
await patchEntryTides(logbookId, entryId, {
|
||||||
highWater: preview.highWater,
|
highWater: preview.highWater,
|
||||||
lowWater: preview.lowWater
|
lowWater: preview.lowWater,
|
||||||
|
...preview.location
|
||||||
})
|
})
|
||||||
setTidePreview(null)
|
setTidePreview(null)
|
||||||
setModal('none')
|
setModal('none')
|
||||||
@@ -1585,24 +1584,9 @@ export default function LiveLogView({
|
|||||||
<p className="live-log-modal-hint" role="note">
|
<p className="live-log-modal-hint" role="note">
|
||||||
{t('logs.tide_disclaimer')}
|
{t('logs.tide_disclaimer')}
|
||||||
</p>
|
</p>
|
||||||
{tidePreview.source === 'departure' && tidePreview.departureQuery ? (
|
{formatTideLocationLabel(tidePreview.location, t) ? (
|
||||||
<p className="live-log-modal-hint" role="status">
|
<p className="live-log-modal-hint" role="status">
|
||||||
{t('logs.tide_fetched_from_departure', {
|
{formatTideLocationLabel(tidePreview.location, t)}
|
||||||
place: tidePreview.placeName || tidePreview.departureQuery
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
) : tidePreview.source === 'gps' ? (
|
|
||||||
<p className="live-log-modal-hint" role="status">
|
|
||||||
{t('logs.tide_fetched_at_position')}
|
|
||||||
</p>
|
|
||||||
) : tidePreview.placeName ? (
|
|
||||||
<p className="live-log-modal-hint" role="status">
|
|
||||||
{tidePreview.distanceKm != null
|
|
||||||
? t('logs.tide_fetched_from', {
|
|
||||||
place: tidePreview.placeName,
|
|
||||||
distance: formatAppDecimal(tidePreview.distanceKm, { maximumFractionDigits: 1 }) ?? String(tidePreview.distanceKm)
|
|
||||||
})
|
|
||||||
: tidePreview.placeName}
|
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<dl className="live-log-tide-preview">
|
<dl className="live-log-tide-preview">
|
||||||
|
|||||||
@@ -44,7 +44,13 @@ import { getLogbookAccess } from '../services/logbookAccess.js'
|
|||||||
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
||||||
import { fetchOpenWeatherCurrent, WeatherApiError } from '../services/weather.js'
|
import { fetchOpenWeatherCurrent, WeatherApiError } from '../services/weather.js'
|
||||||
import { fetchTidesByPlace, fetchTidesNearby, TidesApiError } from '../services/tides.js'
|
import { fetchTidesByPlace, fetchTidesNearby, TidesApiError } from '../services/tides.js'
|
||||||
import { resolveTideFetchLocation } from '../utils/tideLocation.js'
|
import {
|
||||||
|
buildTideLocationMeta,
|
||||||
|
formatTideLocationLabel,
|
||||||
|
pickTideLocationMeta,
|
||||||
|
resolveTideFetchLocation,
|
||||||
|
type TideLocationMeta
|
||||||
|
} from '../utils/tideLocation.js'
|
||||||
import { parseTideTurtleForDate } from '../utils/tideTurtle.js'
|
import { parseTideTurtleForDate } from '../utils/tideTurtle.js'
|
||||||
import {
|
import {
|
||||||
buildTravelDayContext,
|
buildTravelDayContext,
|
||||||
@@ -306,8 +312,8 @@ export default function LogEntryEditor({
|
|||||||
const [tidesCollapsed, setTidesCollapsed] = useState(true)
|
const [tidesCollapsed, setTidesCollapsed] = useState(true)
|
||||||
const [tideHighWater, setTideHighWater] = useState('')
|
const [tideHighWater, setTideHighWater] = useState('')
|
||||||
const [tideLowWater, setTideLowWater] = useState('')
|
const [tideLowWater, setTideLowWater] = useState('')
|
||||||
|
const [tideLocation, setTideLocation] = useState<TideLocationMeta>({})
|
||||||
const [tidesLoading, setTidesLoading] = useState(false)
|
const [tidesLoading, setTidesLoading] = useState(false)
|
||||||
const [tideFetchHint, setTideFetchHint] = useState('')
|
|
||||||
const [tanksCollapsed, setTanksCollapsed] = useState(true)
|
const [tanksCollapsed, setTanksCollapsed] = useState(true)
|
||||||
|
|
||||||
const [columnSelectorOpen, setColumnSelectorOpen] = useState(false)
|
const [columnSelectorOpen, setColumnSelectorOpen] = useState(false)
|
||||||
@@ -440,7 +446,7 @@ export default function LogEntryEditor({
|
|||||||
consumption: parseAppDecimalOrZero(fuelConsumption)
|
consumption: parseAppDecimalOrZero(fuelConsumption)
|
||||||
},
|
},
|
||||||
greywater: { level: parseAppDecimalOrZero(greywaterLevel) },
|
greywater: { level: parseAppDecimalOrZero(greywaterLevel) },
|
||||||
tides: { highWater: tideHighWater, lowWater: tideLowWater },
|
tides: { highWater: tideHighWater, lowWater: tideLowWater, ...tideLocation },
|
||||||
trackDistanceNm: parseOptionalFormDecimal(trackDistanceNm),
|
trackDistanceNm: parseOptionalFormDecimal(trackDistanceNm),
|
||||||
trackSpeedMaxKn: parseOptionalFormDecimal(trackSpeedMaxKn),
|
trackSpeedMaxKn: parseOptionalFormDecimal(trackSpeedMaxKn),
|
||||||
trackSpeedAvgKn: parseOptionalFormDecimal(trackSpeedAvgKn),
|
trackSpeedAvgKn: parseOptionalFormDecimal(trackSpeedAvgKn),
|
||||||
@@ -453,7 +459,7 @@ export default function LogEntryEditor({
|
|||||||
fwMorning, fwRefilled, fwEvening, fwConsumption,
|
fwMorning, fwRefilled, fwEvening, fwConsumption,
|
||||||
fuelMorning, fuelRefilled, fuelEvening, fuelConsumption,
|
fuelMorning, fuelRefilled, fuelEvening, fuelConsumption,
|
||||||
greywaterLevel,
|
greywaterLevel,
|
||||||
tideHighWater, tideLowWater,
|
tideHighWater, tideLowWater, tideLocation,
|
||||||
trackDistanceNm, trackSpeedMaxKn, trackSpeedAvgKn, motorHours,
|
trackDistanceNm, trackSpeedMaxKn, trackSpeedAvgKn, motorHours,
|
||||||
events,
|
events,
|
||||||
entryCrew
|
entryCrew
|
||||||
@@ -504,6 +510,11 @@ export default function LogEntryEditor({
|
|||||||
[fuelMorning, fuelRefilled, tankCapacities.fuelCapacityL]
|
[fuelMorning, fuelRefilled, tankCapacities.fuelCapacityL]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tideLocationLabel = useMemo(
|
||||||
|
() => formatTideLocationLabel(tideLocation, t),
|
||||||
|
[tideLocation, t]
|
||||||
|
)
|
||||||
|
|
||||||
const currentFingerprint = useMemo(() => {
|
const currentFingerprint = useMemo(() => {
|
||||||
const payload = buildPayloadForSigning()
|
const payload = buildPayloadForSigning()
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
@@ -936,7 +947,7 @@ export default function LogEntryEditor({
|
|||||||
const preloadedTides = readLogEntryTides(preloadedEntry as Record<string, unknown>)
|
const preloadedTides = readLogEntryTides(preloadedEntry as Record<string, unknown>)
|
||||||
setTideHighWater(preloadedTides.highWater)
|
setTideHighWater(preloadedTides.highWater)
|
||||||
setTideLowWater(preloadedTides.lowWater)
|
setTideLowWater(preloadedTides.lowWater)
|
||||||
setTideFetchHint('')
|
setTideLocation(pickTideLocationMeta(preloadedTides))
|
||||||
|
|
||||||
setSignSkipper(normalizeSignature(preloadedEntry.signSkipper) || '')
|
setSignSkipper(normalizeSignature(preloadedEntry.signSkipper) || '')
|
||||||
setSignCrew(normalizeSignature(preloadedEntry.signCrew) || '')
|
setSignCrew(normalizeSignature(preloadedEntry.signCrew) || '')
|
||||||
@@ -982,7 +993,7 @@ export default function LogEntryEditor({
|
|||||||
const loadedTides = readLogEntryTides(decrypted as Record<string, unknown>)
|
const loadedTides = readLogEntryTides(decrypted as Record<string, unknown>)
|
||||||
setTideHighWater(loadedTides.highWater)
|
setTideHighWater(loadedTides.highWater)
|
||||||
setTideLowWater(loadedTides.lowWater)
|
setTideLowWater(loadedTides.lowWater)
|
||||||
setTideFetchHint('')
|
setTideLocation(pickTideLocationMeta(loadedTides))
|
||||||
|
|
||||||
setSignSkipper(normalizeSignature(decrypted.signSkipper) || '')
|
setSignSkipper(normalizeSignature(decrypted.signSkipper) || '')
|
||||||
setSignCrew(normalizeSignature(decrypted.signCrew) || '')
|
setSignCrew(normalizeSignature(decrypted.signCrew) || '')
|
||||||
@@ -1300,7 +1311,6 @@ export default function LogEntryEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTidesLoading(true)
|
setTidesLoading(true)
|
||||||
setTideFetchHint('')
|
|
||||||
try {
|
try {
|
||||||
const loaded = await loadEntry(logbookId, entryId)
|
const loaded = await loadEntry(logbookId, entryId)
|
||||||
const eventsForLocation = loaded
|
const eventsForLocation = loaded
|
||||||
@@ -1339,25 +1349,7 @@ export default function LogEntryEditor({
|
|||||||
|
|
||||||
if (parsed.highWater) setTideHighWater(parsed.highWater)
|
if (parsed.highWater) setTideHighWater(parsed.highWater)
|
||||||
if (parsed.lowWater) setTideLowWater(parsed.lowWater)
|
if (parsed.lowWater) setTideLowWater(parsed.lowWater)
|
||||||
|
setTideLocation(buildTideLocationMeta(location, data))
|
||||||
if (location.source === 'departure') {
|
|
||||||
setTideFetchHint(
|
|
||||||
t('logs.tide_fetched_from_departure', {
|
|
||||||
place: parsed.placeName || location.query
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if (location.source === 'gps') {
|
|
||||||
setTideFetchHint(t('logs.tide_fetched_at_position'))
|
|
||||||
} else if (parsed.placeName) {
|
|
||||||
setTideFetchHint(
|
|
||||||
parsed.distanceKm != null
|
|
||||||
? t('logs.tide_fetched_from', {
|
|
||||||
place: parsed.placeName,
|
|
||||||
distance: formatAppDecimal(parsed.distanceKm, { maximumFractionDigits: 1 }) ?? String(parsed.distanceKm)
|
|
||||||
})
|
|
||||||
: parsed.placeName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TidesApiError) {
|
if (err instanceof TidesApiError) {
|
||||||
if (err.code === 'OFFLINE') {
|
if (err.code === 'OFFLINE') {
|
||||||
@@ -2250,9 +2242,9 @@ export default function LogEntryEditor({
|
|||||||
<p className="form-hint" role="note">
|
<p className="form-hint" role="note">
|
||||||
{t('logs.tide_disclaimer')}
|
{t('logs.tide_disclaimer')}
|
||||||
</p>
|
</p>
|
||||||
{tideFetchHint ? (
|
{tideLocationLabel ? (
|
||||||
<p className="form-hint" role="status">
|
<p className="tides-panel__location" role="status">
|
||||||
{tideFetchHint}
|
{tideLocationLabel}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -202,6 +202,9 @@
|
|||||||
"tide_no_data": "Ingen tidevandsdata for dette sted.",
|
"tide_no_data": "Ingen tidevandsdata for dette sted.",
|
||||||
"tide_place_not_found": "“{{place}}” kunne ikke findes — angiv en kystby eller havn.",
|
"tide_place_not_found": "“{{place}}” kunne ikke findes — angiv en kystby eller havn.",
|
||||||
"tide_fetched_at_position": "Modelprognose ved aktuel position (Open-Meteo Marine).",
|
"tide_fetched_at_position": "Modelprognose ved aktuel position (Open-Meteo Marine).",
|
||||||
|
"tide_data_for_position": "Forespørgsel for position {{lat}}, {{lng}}",
|
||||||
|
"tide_data_for_place": "Forespørgsel for {{place}}",
|
||||||
|
"tide_data_for_place_and_position": "Forespørgsel for {{place}} ({{lat}}, {{lng}})",
|
||||||
"tide_fetched_from": "Data fra {{place}} (ca. {{distance}} km væk)",
|
"tide_fetched_from": "Data fra {{place}} (ca. {{distance}} km væk)",
|
||||||
"tide_fetched_from_departure": "Tidevand baseret på afgang “{{place}}” (ingen aktuel GPS-position).",
|
"tide_fetched_from_departure": "Tidevand baseret på afgang “{{place}}” (ingen aktuel GPS-position).",
|
||||||
"tide_applied_success": "Tidevand overført: højvande {{highWater}}, lavvande {{lowWater}}. Synligt i rejsedagseditoren under “Tidevand”.",
|
"tide_applied_success": "Tidevand overført: højvande {{highWater}}, lavvande {{lowWater}}. Synligt i rejsedagseditoren under “Tidevand”.",
|
||||||
|
|||||||
@@ -202,6 +202,9 @@
|
|||||||
"tide_no_data": "Für diesen Ort liegen keine Gezeitendaten vor.",
|
"tide_no_data": "Für diesen Ort liegen keine Gezeitendaten vor.",
|
||||||
"tide_place_not_found": "„{{place}}“ konnte nicht geortet werden — bitte einen Küstenort oder Hafen angeben.",
|
"tide_place_not_found": "„{{place}}“ konnte nicht geortet werden — bitte einen Küstenort oder Hafen angeben.",
|
||||||
"tide_fetched_at_position": "Modellprognose am aktuellen Standort (Open-Meteo Marine).",
|
"tide_fetched_at_position": "Modellprognose am aktuellen Standort (Open-Meteo Marine).",
|
||||||
|
"tide_data_for_position": "Abfrage für Position {{lat}}, {{lng}}",
|
||||||
|
"tide_data_for_place": "Abfrage für {{place}}",
|
||||||
|
"tide_data_for_place_and_position": "Abfrage für {{place}} ({{lat}}, {{lng}})",
|
||||||
"tide_fetched_from": "Daten von {{place}} (ca. {{distance}} km entfernt)",
|
"tide_fetched_from": "Daten von {{place}} (ca. {{distance}} km entfernt)",
|
||||||
"tide_fetched_from_departure": "Gezeiten basierend auf Abfahrtsort „{{place}}“ (keine aktuelle GPS-Position).",
|
"tide_fetched_from_departure": "Gezeiten basierend auf Abfahrtsort „{{place}}“ (keine aktuelle GPS-Position).",
|
||||||
"tide_applied_success": "Gezeiten übernommen: Hochwasser {{highWater}}, Niedrigwasser {{lowWater}}. Im Reisetag-Editor unter „Tiden“ sichtbar.",
|
"tide_applied_success": "Gezeiten übernommen: Hochwasser {{highWater}}, Niedrigwasser {{lowWater}}. Im Reisetag-Editor unter „Tiden“ sichtbar.",
|
||||||
|
|||||||
@@ -202,6 +202,9 @@
|
|||||||
"tide_no_data": "No tide data available for this location.",
|
"tide_no_data": "No tide data available for this location.",
|
||||||
"tide_place_not_found": "“{{place}}” could not be geocoded — please use a coastal place or harbour name.",
|
"tide_place_not_found": "“{{place}}” could not be geocoded — please use a coastal place or harbour name.",
|
||||||
"tide_fetched_at_position": "Model forecast at current position (Open-Meteo Marine).",
|
"tide_fetched_at_position": "Model forecast at current position (Open-Meteo Marine).",
|
||||||
|
"tide_data_for_position": "Query for position {{lat}}, {{lng}}",
|
||||||
|
"tide_data_for_place": "Query for {{place}}",
|
||||||
|
"tide_data_for_place_and_position": "Query for {{place}} ({{lat}}, {{lng}})",
|
||||||
"tide_fetched_from": "Data from {{place}} (about {{distance}} km away)",
|
"tide_fetched_from": "Data from {{place}} (about {{distance}} km away)",
|
||||||
"tide_fetched_from_departure": "Tides based on departure “{{place}}” (no current GPS position).",
|
"tide_fetched_from_departure": "Tides based on departure “{{place}}” (no current GPS position).",
|
||||||
"tide_applied_success": "Tides applied: high water {{highWater}}, low water {{lowWater}}. Visible in the travel day editor under “Tides”.",
|
"tide_applied_success": "Tides applied: high water {{highWater}}, low water {{lowWater}}. Visible in the travel day editor under “Tides”.",
|
||||||
|
|||||||
@@ -202,6 +202,9 @@
|
|||||||
"tide_no_data": "No hay datos de marea para este lugar.",
|
"tide_no_data": "No hay datos de marea para este lugar.",
|
||||||
"tide_place_not_found": "«{{place}}» no se encontró — indica un lugar costero o puerto.",
|
"tide_place_not_found": "«{{place}}» no se encontró — indica un lugar costero o puerto.",
|
||||||
"tide_fetched_at_position": "Pronóstico modelo en la posición actual (Open-Meteo Marine).",
|
"tide_fetched_at_position": "Pronóstico modelo en la posición actual (Open-Meteo Marine).",
|
||||||
|
"tide_data_for_position": "Consulta para la posición {{lat}}, {{lng}}",
|
||||||
|
"tide_data_for_place": "Consulta para {{place}}",
|
||||||
|
"tide_data_for_place_and_position": "Consulta para {{place}} ({{lat}}, {{lng}})",
|
||||||
"tide_fetched_from": "Datos de {{place}} (aprox. {{distance}} km)",
|
"tide_fetched_from": "Datos de {{place}} (aprox. {{distance}} km)",
|
||||||
"tide_fetched_from_departure": "Mareas según salida «{{place}}» (sin posición GPS actual).",
|
"tide_fetched_from_departure": "Mareas según salida «{{place}}» (sin posición GPS actual).",
|
||||||
"tide_applied_success": "Mareas guardadas: pleamar {{highWater}}, bajamar {{lowWater}}. Visible en el editor del día de viaje, sección «Mareas».",
|
"tide_applied_success": "Mareas guardadas: pleamar {{highWater}}, bajamar {{lowWater}}. Visible en el editor del día de viaje, sección «Mareas».",
|
||||||
|
|||||||
@@ -202,6 +202,9 @@
|
|||||||
"tide_no_data": "Aucune donnée de marée pour cet endroit.",
|
"tide_no_data": "Aucune donnée de marée pour cet endroit.",
|
||||||
"tide_place_not_found": "« {{place}} » introuvable — indiquez un lieu côtier ou un port.",
|
"tide_place_not_found": "« {{place}} » introuvable — indiquez un lieu côtier ou un port.",
|
||||||
"tide_fetched_at_position": "Prévision modèle à la position actuelle (Open-Meteo Marine).",
|
"tide_fetched_at_position": "Prévision modèle à la position actuelle (Open-Meteo Marine).",
|
||||||
|
"tide_data_for_position": "Requête pour la position {{lat}}, {{lng}}",
|
||||||
|
"tide_data_for_place": "Requête pour {{place}}",
|
||||||
|
"tide_data_for_place_and_position": "Requête pour {{place}} ({{lat}}, {{lng}})",
|
||||||
"tide_fetched_from": "Données de {{place}} (env. {{distance}} km)",
|
"tide_fetched_from": "Données de {{place}} (env. {{distance}} km)",
|
||||||
"tide_fetched_from_departure": "Marées basées sur le départ « {{place}} » (pas de position GPS actuelle).",
|
"tide_fetched_from_departure": "Marées basées sur le départ « {{place}} » (pas de position GPS actuelle).",
|
||||||
"tide_applied_success": "Marées enregistrées : pleine mer {{highWater}}, basse mer {{lowWater}}. Visible dans l’éditeur du jour de voyage, section « Marées ».",
|
"tide_applied_success": "Marées enregistrées : pleine mer {{highWater}}, basse mer {{lowWater}}. Visible dans l’éditeur du jour de voyage, section « Marées ».",
|
||||||
|
|||||||
@@ -202,6 +202,9 @@
|
|||||||
"tide_no_data": "Ingen tidevannsdata for dette stedet.",
|
"tide_no_data": "Ingen tidevannsdata for dette stedet.",
|
||||||
"tide_place_not_found": "«{{place}}» ble ikke funnet — oppgi en kyststad eller havn.",
|
"tide_place_not_found": "«{{place}}» ble ikke funnet — oppgi en kyststad eller havn.",
|
||||||
"tide_fetched_at_position": "Modellprognose ved gjeldende posisjon (Open-Meteo Marine).",
|
"tide_fetched_at_position": "Modellprognose ved gjeldende posisjon (Open-Meteo Marine).",
|
||||||
|
"tide_data_for_position": "Forespørsel for posisjon {{lat}}, {{lng}}",
|
||||||
|
"tide_data_for_place": "Forespørsel for {{place}}",
|
||||||
|
"tide_data_for_place_and_position": "Forespørsel for {{place}} ({{lat}}, {{lng}})",
|
||||||
"tide_fetched_from": "Data fra {{place}} (ca. {{distance}} km unna)",
|
"tide_fetched_from": "Data fra {{place}} (ca. {{distance}} km unna)",
|
||||||
"tide_fetched_from_departure": "Tidevann basert på avreise «{{place}}» (ingen aktuell GPS-posisjon).",
|
"tide_fetched_from_departure": "Tidevann basert på avreise «{{place}}» (ingen aktuell GPS-posisjon).",
|
||||||
"tide_applied_success": "Tidevann lagret: høyvann {{highWater}}, lavvann {{lowWater}}. Synlig i reisedagseditoren under «Tidevann».",
|
"tide_applied_success": "Tidevann lagret: høyvann {{highWater}}, lavvann {{lowWater}}. Synlig i reisedagseditoren under «Tidevann».",
|
||||||
|
|||||||
@@ -202,6 +202,9 @@
|
|||||||
"tide_no_data": "Inga tidvattendata för denna plats.",
|
"tide_no_data": "Inga tidvattendata för denna plats.",
|
||||||
"tide_place_not_found": "“{{place}}” kunde inte hittas — ange en kustort eller hamn.",
|
"tide_place_not_found": "“{{place}}” kunde inte hittas — ange en kustort eller hamn.",
|
||||||
"tide_fetched_at_position": "Modellprognos vid aktuell position (Open-Meteo Marine).",
|
"tide_fetched_at_position": "Modellprognos vid aktuell position (Open-Meteo Marine).",
|
||||||
|
"tide_data_for_position": "Förfrågan för position {{lat}}, {{lng}}",
|
||||||
|
"tide_data_for_place": "Förfrågan för {{place}}",
|
||||||
|
"tide_data_for_place_and_position": "Förfrågan för {{place}} ({{lat}}, {{lng}})",
|
||||||
"tide_fetched_from": "Data från {{place}} (ca {{distance}} km bort)",
|
"tide_fetched_from": "Data från {{place}} (ca {{distance}} km bort)",
|
||||||
"tide_fetched_from_departure": "Tidvatten baserat på avgång “{{place}}” (ingen aktuell GPS-position).",
|
"tide_fetched_from_departure": "Tidvatten baserat på avgång “{{place}}” (ingen aktuell GPS-position).",
|
||||||
"tide_applied_success": "Tidvatten tillämpat: högvatten {{highWater}}, lågvatten {{lowWater}}. Syns i resedagseditorn under “Tidvatten”.",
|
"tide_applied_success": "Tidvatten tillämpat: högvatten {{highWater}}, lågvatten {{lowWater}}. Syns i resedagseditorn under “Tidvatten”.",
|
||||||
|
|||||||
@@ -91,4 +91,24 @@ describe('buildLogEntryPayload tides', () => {
|
|||||||
})
|
})
|
||||||
expect(payload.tides).toEqual({ highWater: '18:34', lowWater: '12:05' })
|
expect(payload.tides).toEqual({ highWater: '18:34', lowWater: '12:05' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('persists tide location metadata', () => {
|
||||||
|
const payload = buildLogEntryPayload({
|
||||||
|
...base,
|
||||||
|
tides: {
|
||||||
|
highWater: '06:00',
|
||||||
|
lowWater: '00:04',
|
||||||
|
locationSource: 'gps',
|
||||||
|
lat: '53.624526',
|
||||||
|
lng: '7.155263'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(payload.tides).toEqual({
|
||||||
|
highWater: '06:00',
|
||||||
|
lowWater: '00:04',
|
||||||
|
locationSource: 'gps',
|
||||||
|
lat: '53.624526',
|
||||||
|
lng: '7.155263'
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -150,9 +150,15 @@ export function sortLogEventsByTime<T extends LogEventPayload>(events: T[]): T[]
|
|||||||
return [...events].sort((a, b) => (a.time || '').localeCompare(b.time || ''))
|
return [...events].sort((a, b) => (a.time || '').localeCompare(b.time || ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TideLocationSource = 'gps' | 'departure' | 'geocoded'
|
||||||
|
|
||||||
export interface LogEntryTides {
|
export interface LogEntryTides {
|
||||||
highWater: string
|
highWater: string
|
||||||
lowWater: string
|
lowWater: string
|
||||||
|
locationSource?: TideLocationSource
|
||||||
|
placeName?: string
|
||||||
|
lat?: string
|
||||||
|
lng?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogEntryPayloadInput {
|
export interface LogEntryPayloadInput {
|
||||||
@@ -172,13 +178,28 @@ export interface LogEntryPayloadInput {
|
|||||||
entryCrew?: EntryCrewFields
|
entryCrew?: EntryCrewFields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readTideLocationSource(value: unknown): TideLocationSource | undefined {
|
||||||
|
const source = String(value ?? '').trim()
|
||||||
|
if (source === 'gps' || source === 'departure' || source === 'geocoded') return source
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function readLogEntryTides(data: Record<string, unknown>): LogEntryTides {
|
export function readLogEntryTides(data: Record<string, unknown>): LogEntryTides {
|
||||||
const tides = data.tides as Record<string, unknown> | undefined
|
const tides = data.tides as Record<string, unknown> | undefined
|
||||||
const highRaw = String(tides?.highWater ?? '').trim()
|
const highRaw = String(tides?.highWater ?? '').trim()
|
||||||
const lowRaw = String(tides?.lowWater ?? '').trim()
|
const lowRaw = String(tides?.lowWater ?? '').trim()
|
||||||
|
const placeName = String(tides?.placeName ?? '').trim()
|
||||||
|
const lat = String(tides?.lat ?? '').trim()
|
||||||
|
const lng = String(tides?.lng ?? '').trim()
|
||||||
|
const locationSource = readTideLocationSource(tides?.locationSource)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
highWater: parseTimeToHHMM(highRaw) ?? '',
|
highWater: parseTimeToHHMM(highRaw) ?? '',
|
||||||
lowWater: parseTimeToHHMM(lowRaw) ?? ''
|
lowWater: parseTimeToHHMM(lowRaw) ?? '',
|
||||||
|
...(locationSource ? { locationSource } : {}),
|
||||||
|
...(placeName ? { placeName } : {}),
|
||||||
|
...(lat ? { lat } : {}),
|
||||||
|
...(lng ? { lng } : {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +232,15 @@ export function buildLogEntryPayload(input: LogEntryPayloadInput): Record<string
|
|||||||
const highWater = parseTimeToHHMM(input.tides.highWater) ?? ''
|
const highWater = parseTimeToHHMM(input.tides.highWater) ?? ''
|
||||||
const lowWater = parseTimeToHHMM(input.tides.lowWater) ?? ''
|
const lowWater = parseTimeToHHMM(input.tides.lowWater) ?? ''
|
||||||
if (highWater || lowWater) {
|
if (highWater || lowWater) {
|
||||||
payload.tides = { highWater, lowWater }
|
const tides: Record<string, string> = { highWater, lowWater }
|
||||||
|
if (input.tides.locationSource) tides.locationSource = input.tides.locationSource
|
||||||
|
const placeName = input.tides.placeName?.trim()
|
||||||
|
if (placeName) tides.placeName = placeName
|
||||||
|
const lat = input.tides.lat?.trim()
|
||||||
|
if (lat) tides.lat = lat
|
||||||
|
const lng = input.tides.lng?.trim()
|
||||||
|
if (lng) tides.lng = lng
|
||||||
|
payload.tides = tides
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { LIVE_EVENT_CODES } from './liveEventCodes.js'
|
import { LIVE_EVENT_CODES } from './liveEventCodes.js'
|
||||||
import { resolveTideFetchLocation } from './tideLocation.js'
|
import {
|
||||||
|
buildTideLocationMeta,
|
||||||
|
formatTideLocationLabel,
|
||||||
|
resolveTideFetchLocation
|
||||||
|
} from './tideLocation.js'
|
||||||
|
|
||||||
const entryDate = '2026-06-11'
|
const entryDate = '2026-06-11'
|
||||||
const nowMs = new Date('2026-06-11T12:00:00').getTime()
|
const nowMs = new Date('2026-06-11T12:00:00').getTime()
|
||||||
@@ -108,6 +112,33 @@ describe('resolveTideFetchLocation', () => {
|
|||||||
expect(result).toEqual({ error: 'stale' })
|
expect(result).toEqual({ error: 'stale' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('builds GPS location metadata from nearby fetch', () => {
|
||||||
|
const meta = buildTideLocationMeta(
|
||||||
|
{ mode: 'nearby', lat: '53.624526', lng: '7.155263', source: 'gps' },
|
||||||
|
{ location: { name: 'Norddeich', lat: 53.62, lon: 7.15, source: 'coordinates' } }
|
||||||
|
)
|
||||||
|
expect(meta).toEqual({
|
||||||
|
locationSource: 'gps',
|
||||||
|
lat: '53.624526',
|
||||||
|
lng: '7.155263',
|
||||||
|
placeName: 'Norddeich'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats coordinate and place labels', () => {
|
||||||
|
const t = (key: string, options?: Record<string, string>) =>
|
||||||
|
`${key}:${JSON.stringify(options ?? {})}`
|
||||||
|
expect(
|
||||||
|
formatTideLocationLabel(
|
||||||
|
{ locationSource: 'gps', lat: '53.62', lng: '7.15', placeName: 'Norddeich' },
|
||||||
|
t
|
||||||
|
)
|
||||||
|
).toContain('tide_data_for_place_and_position')
|
||||||
|
expect(
|
||||||
|
formatTideLocationLabel({ locationSource: 'gps', lat: '53.62', lng: '7.15' }, t)
|
||||||
|
).toContain('tide_data_for_position')
|
||||||
|
})
|
||||||
|
|
||||||
it('returns missing without position or departure', () => {
|
it('returns missing without position or departure', () => {
|
||||||
const result = resolveTideFetchLocation({
|
const result = resolveTideFetchLocation({
|
||||||
events: [],
|
events: [],
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import {
|
|||||||
getLatestLoggedPosition,
|
getLatestLoggedPosition,
|
||||||
LIVE_LOG_TIDE_POSITION_MAX_AGE_MS
|
LIVE_LOG_TIDE_POSITION_MAX_AGE_MS
|
||||||
} from './liveEventCodes.js'
|
} from './liveEventCodes.js'
|
||||||
import type { LogEventPayload } from './logEntryPayload.js'
|
import type { LogEntryTides, LogEventPayload, TideLocationSource } from './logEntryPayload.js'
|
||||||
|
|
||||||
export type TideLocationSource = 'gps' | 'departure'
|
export type { TideLocationSource }
|
||||||
|
|
||||||
|
export type TideLocationMeta = Pick<LogEntryTides, 'locationSource' | 'placeName' | 'lat' | 'lng'>
|
||||||
|
|
||||||
export type TideFetchLocation =
|
export type TideFetchLocation =
|
||||||
| { mode: 'nearby'; lat: string; lng: string; source: 'gps' }
|
| { mode: 'nearby'; lat: string; lng: string; source: 'gps' }
|
||||||
@@ -45,3 +47,73 @@ export function resolveTideFetchLocation(options: {
|
|||||||
|
|
||||||
return { error: 'missing' }
|
return { error: 'missing' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTideLocationMeta(
|
||||||
|
fetchLocation: TideFetchLocation,
|
||||||
|
apiData: Record<string, unknown>
|
||||||
|
): TideLocationMeta {
|
||||||
|
const apiLocation = asRecord(apiData.location)
|
||||||
|
|
||||||
|
if (fetchLocation.mode === 'nearby') {
|
||||||
|
return {
|
||||||
|
locationSource: 'gps',
|
||||||
|
lat: fetchLocation.lat,
|
||||||
|
lng: fetchLocation.lng,
|
||||||
|
placeName: apiLocation?.name ? String(apiLocation.name) : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeName = apiLocation?.name ? String(apiLocation.name) : fetchLocation.query
|
||||||
|
const lat = apiLocation?.lat != null && apiLocation.lat !== '' ? String(apiLocation.lat) : undefined
|
||||||
|
const lng = apiLocation?.lon != null && apiLocation.lon !== '' ? String(apiLocation.lon) : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
locationSource: apiLocation?.source === 'geocoded' ? 'geocoded' : 'departure',
|
||||||
|
placeName,
|
||||||
|
lat,
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TideLocationLabelT = (
|
||||||
|
key: string,
|
||||||
|
options?: Record<string, string | undefined>
|
||||||
|
) => string
|
||||||
|
|
||||||
|
export function formatTideLocationLabel(
|
||||||
|
tides: TideLocationMeta,
|
||||||
|
t: TideLocationLabelT
|
||||||
|
): string {
|
||||||
|
const placeName = tides.placeName?.trim()
|
||||||
|
const lat = tides.lat?.trim()
|
||||||
|
const lng = tides.lng?.trim()
|
||||||
|
|
||||||
|
if (placeName && lat && lng) {
|
||||||
|
return t('logs.tide_data_for_place_and_position', { place: placeName, lat, lng })
|
||||||
|
}
|
||||||
|
if (lat && lng) {
|
||||||
|
return t('logs.tide_data_for_position', { lat, lng })
|
||||||
|
}
|
||||||
|
if (placeName) {
|
||||||
|
if (tides.locationSource === 'departure') {
|
||||||
|
return t('logs.tide_fetched_from_departure', { place: placeName })
|
||||||
|
}
|
||||||
|
return t('logs.tide_data_for_place', { place: placeName })
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickTideLocationMeta(tides: LogEntryTides): TideLocationMeta {
|
||||||
|
return {
|
||||||
|
locationSource: tides.locationSource,
|
||||||
|
placeName: tides.placeName,
|
||||||
|
lat: tides.lat,
|
||||||
|
lng: tides.lng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user