feat: BSH-Pegelauswahl und Fix für Eintragstag beim Gezeiten-Abruf
Bei fehlgeschlagenem Auto-Abruf nächste BSH-Stationen anbieten; Reisetag korrekt aus dem Eintrag parsen und Vergangenheitshinweis anzeigen. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -58,13 +58,18 @@ const formatSpeedKn = (speedKn: number) =>
|
||||
formatAppDecimal(speedKn, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
|
||||
import { parseOwmCurrentWeather } from '../utils/openWeatherMap.js'
|
||||
import { fetchOpenWeatherCurrent, WeatherApiError } from '../services/weather.js'
|
||||
import { fetchTidesByPlace, fetchTidesNearby, TidesApiError } from '../services/tides.js'
|
||||
import { TidesApiError, type TideStation } from '../services/tides.js'
|
||||
import { TideStationPickerModal } from './TideStationPickerModal.tsx'
|
||||
import {
|
||||
buildTideLocationMeta,
|
||||
formatTideLocationLabel,
|
||||
resolveTideFetchLocation
|
||||
} from '../utils/tideLocation.js'
|
||||
import { parseTideTurtleForDate } from '../utils/tideTurtle.js'
|
||||
import {
|
||||
fetchTidesForEntry,
|
||||
fetchTidesForStationChoice,
|
||||
type TideFetchNeedsStationPick
|
||||
} from '../utils/tideFetch.js'
|
||||
import {
|
||||
geolocationErrorI18nKey,
|
||||
getCurrentPosition,
|
||||
@@ -217,6 +222,7 @@ export default function LiveLogView({
|
||||
lowWater: string
|
||||
location: ReturnType<typeof buildTideLocationMeta>
|
||||
} | null>(null)
|
||||
const [tideStationPicker, setTideStationPicker] = useState<TideFetchNeedsStationPick | null>(null)
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
||||
const [commentText, setCommentText] = useState('')
|
||||
const [valueInput, setValueInput] = useState('')
|
||||
@@ -802,6 +808,38 @@ export default function LiveLogView({
|
||||
})()
|
||||
}
|
||||
|
||||
const handleTideStationPick = (pick: TideFetchNeedsStationPick, station: TideStation) => {
|
||||
setTidesLoading(true)
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await fetchTidesForStationChoice({
|
||||
stationId: station.id,
|
||||
entryDate: pick.entryDate,
|
||||
fetchLocation: pick.fetchLocation,
|
||||
queryLat: pick.queryLat,
|
||||
queryLng: pick.queryLng,
|
||||
analyticsSource: 'live_log'
|
||||
})
|
||||
setTideStationPicker(null)
|
||||
setTidePreview({
|
||||
highWater: result.highWater,
|
||||
lowWater: result.lowWater,
|
||||
location: result.location
|
||||
})
|
||||
setModal('tides')
|
||||
} catch (err) {
|
||||
if (err instanceof TidesApiError && err.code === 'NO_DATA_FOR_DATE') {
|
||||
void showAlert(t('logs.tide_no_data_for_date', { date: pick.entryDate }), t('logs.tides'))
|
||||
return
|
||||
}
|
||||
console.error('Live log tide station fetch failed:', err)
|
||||
void showAlert(t('logs.tide_fetch_failed'), t('logs.tides'))
|
||||
} finally {
|
||||
setTidesLoading(false)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
const handleFetchTides = () => {
|
||||
if (!entryId || busy || tidesLoading) return
|
||||
if (!isOnline) {
|
||||
@@ -835,24 +873,21 @@ export default function LiveLogView({
|
||||
return
|
||||
}
|
||||
|
||||
const data =
|
||||
location.mode === 'nearby'
|
||||
? await fetchTidesNearby(location.lat, location.lng, {
|
||||
analyticsSource: 'live_log',
|
||||
locationSource: location.source
|
||||
})
|
||||
: await fetchTidesByPlace(location.query, { analyticsSource: 'live_log' })
|
||||
const outcome = await fetchTidesForEntry({
|
||||
fetchLocation: location,
|
||||
entryDate: entryDateForLocation,
|
||||
analyticsSource: 'live_log'
|
||||
})
|
||||
|
||||
const parsed = parseTideTurtleForDate(data, date)
|
||||
if (!parsed.highWater && !parsed.lowWater) {
|
||||
void showAlert(t('logs.tide_no_data'), t('logs.tides'))
|
||||
if (outcome.kind === 'pick_station') {
|
||||
setTideStationPicker(outcome)
|
||||
return
|
||||
}
|
||||
|
||||
setTidePreview({
|
||||
highWater: parsed.highWater,
|
||||
lowWater: parsed.lowWater,
|
||||
location: buildTideLocationMeta(location, data)
|
||||
highWater: outcome.highWater,
|
||||
lowWater: outcome.lowWater,
|
||||
location: outcome.location
|
||||
})
|
||||
setModal('tides')
|
||||
} catch (err) {
|
||||
@@ -865,6 +900,10 @@ export default function LiveLogView({
|
||||
void showAlert(t('logs.tide_place_not_found', { place: departure.trim() }), t('logs.tides'))
|
||||
return
|
||||
}
|
||||
if (err.code === 'NO_DATA_FOR_DATE') {
|
||||
void showAlert(t('logs.tide_no_data_for_date', { date }), t('logs.tides'))
|
||||
return
|
||||
}
|
||||
if (err.code === 'NOT_FOUND') {
|
||||
void showAlert(t('logs.tide_no_data'), t('logs.tides'))
|
||||
return
|
||||
@@ -1574,6 +1613,17 @@ export default function LiveLogView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tideStationPicker ? (
|
||||
<TideStationPickerModal
|
||||
title={t('logs.tide_pick_station_title')}
|
||||
hint={t('logs.tide_pick_station_hint')}
|
||||
cancelLabel={t('logs.live_cancel')}
|
||||
stations={tideStationPicker.stations}
|
||||
onCancel={() => setTideStationPicker(null)}
|
||||
onSelect={(station) => handleTideStationPick(tideStationPicker, station)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{modal === 'tides' && tidePreview && (
|
||||
<div
|
||||
className="live-log-modal-backdrop"
|
||||
|
||||
@@ -43,15 +43,19 @@ import { putEntryRecord } from '../utils/entryListCache.js'
|
||||
import { getLogbookAccess } from '../services/logbookAccess.js'
|
||||
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
||||
import { fetchOpenWeatherCurrent, WeatherApiError } from '../services/weather.js'
|
||||
import { fetchTidesByPlace, fetchTidesNearby, TidesApiError } from '../services/tides.js'
|
||||
import { TidesApiError, type TideStation } from '../services/tides.js'
|
||||
import { TideStationPickerModal } from './TideStationPickerModal.tsx'
|
||||
import {
|
||||
buildTideLocationMeta,
|
||||
formatTideLocationLabel,
|
||||
pickTideLocationMeta,
|
||||
resolveTideFetchLocation,
|
||||
type TideLocationMeta
|
||||
} from '../utils/tideLocation.js'
|
||||
import { parseTideTurtleForDate } from '../utils/tideTurtle.js'
|
||||
import {
|
||||
fetchTidesForEntry,
|
||||
fetchTidesForStationChoice,
|
||||
type TideFetchNeedsStationPick
|
||||
} from '../utils/tideFetch.js'
|
||||
import {
|
||||
buildTravelDayContext,
|
||||
fetchTravelDaySummaryUsage,
|
||||
@@ -313,6 +317,7 @@ export default function LogEntryEditor({
|
||||
const [tideLowWater, setTideLowWater] = useState('')
|
||||
const [tideLocation, setTideLocation] = useState<TideLocationMeta>({})
|
||||
const [tidesLoading, setTidesLoading] = useState(false)
|
||||
const [tideStationPicker, setTideStationPicker] = useState<TideFetchNeedsStationPick | null>(null)
|
||||
const [tanksCollapsed, setTanksCollapsed] = useState(true)
|
||||
|
||||
const [columnSelectorOpen, setColumnSelectorOpen] = useState(false)
|
||||
@@ -1303,6 +1308,41 @@ export default function LogEntryEditor({
|
||||
}
|
||||
}
|
||||
|
||||
const applyTideFetchResult = (result: {
|
||||
highWater: string
|
||||
lowWater: string
|
||||
location: TideLocationMeta
|
||||
}) => {
|
||||
if (result.highWater) setTideHighWater(result.highWater)
|
||||
if (result.lowWater) setTideLowWater(result.lowWater)
|
||||
setTideLocation(result.location)
|
||||
}
|
||||
|
||||
const handleTideStationPick = async (pick: TideFetchNeedsStationPick, station: TideStation) => {
|
||||
setTidesLoading(true)
|
||||
try {
|
||||
const result = await fetchTidesForStationChoice({
|
||||
stationId: station.id,
|
||||
entryDate: pick.entryDate,
|
||||
fetchLocation: pick.fetchLocation,
|
||||
queryLat: pick.queryLat,
|
||||
queryLng: pick.queryLng,
|
||||
analyticsSource: 'entry_editor'
|
||||
})
|
||||
applyTideFetchResult(result)
|
||||
setTideStationPicker(null)
|
||||
} catch (err) {
|
||||
if (err instanceof TidesApiError && err.code === 'NO_DATA_FOR_DATE') {
|
||||
showAlert(t('logs.tide_no_data_for_date', { date: pick.entryDate }), t('logs.tide_fetch_btn'))
|
||||
return
|
||||
}
|
||||
console.error('Tide station fetch failed:', err)
|
||||
showAlert(t('logs.tide_fetch_failed'), t('logs.tide_fetch_btn'))
|
||||
} finally {
|
||||
setTidesLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFetchTides = async () => {
|
||||
if (!isOnline) {
|
||||
showAlert(t('logs.weather_offline'), t('logs.tide_fetch_btn'))
|
||||
@@ -1332,23 +1372,18 @@ export default function LogEntryEditor({
|
||||
return
|
||||
}
|
||||
|
||||
const data =
|
||||
location.mode === 'nearby'
|
||||
? await fetchTidesNearby(location.lat, location.lng, {
|
||||
analyticsSource: 'entry_editor',
|
||||
locationSource: location.source
|
||||
})
|
||||
: await fetchTidesByPlace(location.query, { analyticsSource: 'entry_editor' })
|
||||
const outcome = await fetchTidesForEntry({
|
||||
fetchLocation: location,
|
||||
entryDate: entryDateForLocation,
|
||||
analyticsSource: 'entry_editor'
|
||||
})
|
||||
|
||||
const parsed = parseTideTurtleForDate(data, date)
|
||||
if (!parsed.highWater && !parsed.lowWater) {
|
||||
showAlert(t('logs.tide_no_data'), t('logs.tide_fetch_btn'))
|
||||
if (outcome.kind === 'pick_station') {
|
||||
setTideStationPicker(outcome)
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.highWater) setTideHighWater(parsed.highWater)
|
||||
if (parsed.lowWater) setTideLowWater(parsed.lowWater)
|
||||
setTideLocation(buildTideLocationMeta(location, data))
|
||||
applyTideFetchResult(outcome)
|
||||
} catch (err) {
|
||||
if (err instanceof TidesApiError) {
|
||||
if (err.code === 'OFFLINE') {
|
||||
@@ -1359,6 +1394,10 @@ export default function LogEntryEditor({
|
||||
showAlert(t('logs.tide_place_not_found', { place: departure.trim() }), t('logs.tide_fetch_btn'))
|
||||
return
|
||||
}
|
||||
if (err.code === 'NO_DATA_FOR_DATE') {
|
||||
showAlert(t('logs.tide_no_data_for_date', { date }), t('logs.tide_fetch_btn'))
|
||||
return
|
||||
}
|
||||
if (err.code === 'NOT_FOUND') {
|
||||
showAlert(t('logs.tide_no_data'), t('logs.tide_fetch_btn'))
|
||||
return
|
||||
@@ -3101,6 +3140,19 @@ export default function LogEntryEditor({
|
||||
nmeaArchive={nmeaArchive}
|
||||
onImport={handleNmeaImport}
|
||||
/>
|
||||
|
||||
{tideStationPicker ? (
|
||||
<TideStationPickerModal
|
||||
title={t('logs.tide_pick_station_title')}
|
||||
hint={t('logs.tide_pick_station_hint')}
|
||||
cancelLabel={t('logs.live_cancel')}
|
||||
stations={tideStationPicker.stations}
|
||||
onCancel={() => setTideStationPicker(null)}
|
||||
onSelect={(station) => {
|
||||
void handleTideStationPick(tideStationPicker, station)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { TideStation } from '../services/tides.js'
|
||||
|
||||
type TideStationPickerModalProps = {
|
||||
title: string
|
||||
hint: string
|
||||
cancelLabel: string
|
||||
stations: TideStation[]
|
||||
onSelect: (station: TideStation) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function TideStationPickerModal({
|
||||
title,
|
||||
hint,
|
||||
cancelLabel,
|
||||
stations,
|
||||
onSelect,
|
||||
onCancel
|
||||
}: TideStationPickerModalProps) {
|
||||
return (
|
||||
<div
|
||||
className="live-log-modal-backdrop"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onCancel()
|
||||
}}
|
||||
>
|
||||
<div className="live-log-modal tide-station-picker" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>{title}</h3>
|
||||
<p className="live-log-modal-hint" role="note">
|
||||
{hint}
|
||||
</p>
|
||||
<ul className="tide-station-picker__list">
|
||||
{stations.map((station) => (
|
||||
<li key={station.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="tide-station-picker__option"
|
||||
onClick={() => onSelect(station)}
|
||||
>
|
||||
<span className="tide-station-picker__name">{station.name}</span>
|
||||
<span className="tide-station-picker__meta">
|
||||
{station.distanceKm} km
|
||||
{station.area ? ` · ${station.area}` : ''}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="live-log-modal-actions">
|
||||
<button type="button" className="btn secondary" onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user