feat(tides): support role-based multi-location tide retrieval, selection, and storage

This commit is contained in:
2026-06-12 13:58:38 +02:00
parent abd5fe1ac8
commit 5b9c1e3220
15 changed files with 623 additions and 151 deletions
+70 -28
View File
@@ -60,11 +60,15 @@ import { parseOwmCurrentWeather } from '../utils/openWeatherMap.js'
import { fetchOpenWeatherCurrent, WeatherApiError } from '../services/weather.js'
import { TidesApiError, type TideStation } from '../services/tides.js'
import { TideStationPickerModal } from './TideStationPickerModal.tsx'
import { TideLocationPickerModal } from './TideLocationPickerModal.tsx'
import {
buildTideLocationMeta,
formatTideLocationLabel,
resolveTideFetchLocation
getAvailableTideLocations,
type TideLocationOption,
type TideFetchLocation
} from '../utils/tideLocation.js'
import type { TideRole } from '../utils/logEntryPayload.js'
import {
fetchTidesForEntry,
fetchTidesForStationChoice,
@@ -124,6 +128,7 @@ type LiveModal =
| 'stw'
| 'position'
| 'tides'
| 'tides_picker'
| 'photo'
| 'voice'
@@ -207,6 +212,7 @@ export default function LiveLogView({
const [dayOfTravel, setDayOfTravel] = useState('')
const [date, setDate] = useState('')
const [departure, setDeparture] = useState('')
const [destination, setDestination] = useState('')
const [events, setEvents] = useState<LogEventPayload[]>([])
const [crewSnapshotsById, setCrewSnapshotsById] = useState<Record<string, any>>({})
const [selectedSkipperId, setSelectedSkipperId] = useState<string | null>(null)
@@ -222,8 +228,10 @@ export default function LiveLogView({
highWater: string
lowWater: string
location: ReturnType<typeof buildTideLocationMeta>
role: TideRole
} | null>(null)
const [tideStationPicker, setTideStationPicker] = useState<TideFetchNeedsStationPick | null>(null)
const [tideLocationPickerOptions, setTideLocationPickerOptions] = useState<TideLocationOption[] | null>(null)
const [isOnline, setIsOnline] = useState(navigator.onLine)
const [commentText, setCommentText] = useState('')
const [valueInput, setValueInput] = useState('')
@@ -326,6 +334,7 @@ export default function LiveLogView({
setDayOfTravel(String(loaded.data.dayOfTravel || ''))
setDate(String(loaded.data.date || ''))
setDeparture(String(loaded.data.departure || ''))
setDestination(String(loaded.data.destination || ''))
setEvents(sortLogEventsByTime(entryEvents.map((e) => ({ ...e }))))
setCrewSnapshotsById((loaded.data.crewSnapshotsById as Record<string, any>) || {})
setSelectedSkipperId(typeof loaded.data.selectedSkipperId === 'string' ? loaded.data.selectedSkipperId : null)
@@ -809,6 +818,12 @@ export default function LiveLogView({
})()
}
const getRoleForLocationSource = (source: string): TideRole => {
if (source === 'gps') return 'gps'
if (source === 'destination') return 'destination'
return 'departure'
}
const handleTideStationPick = (pick: TideFetchNeedsStationPick, station: TideStation) => {
setTidesLoading(true)
void (async () => {
@@ -825,7 +840,8 @@ export default function LiveLogView({
setTidePreview({
highWater: result.highWater,
lowWater: result.lowWater,
location: result.location
location: result.location,
role: getRoleForLocationSource(pick.fetchLocation.source)
})
setModal('tides')
} catch (err) {
@@ -841,34 +857,13 @@ export default function LiveLogView({
})()
}
const handleFetchTides = () => {
if (!entryId || busy || tidesLoading) return
if (!isOnline) {
void showAlert(t('logs.weather_offline'), t('logs.tides'))
return
}
const startTideFetchForLocation = (fetchLocation: TideFetchLocation) => {
setTidesLoading(true)
setError(null)
void (async () => {
try {
const location = resolveTideFetchLocation({
events,
entryDate: date,
departure
})
if ('error' in location) {
void showAlert(
location.error === 'stale'
? t('logs.tide_position_stale')
: t('logs.tide_location_required'),
t('logs.tides')
)
return
}
const outcome = await fetchTidesForEntry({
fetchLocation: location,
fetchLocation,
entryDate: date,
analyticsSource: 'live_log'
})
@@ -882,7 +877,8 @@ export default function LiveLogView({
setTidePreview({
highWater: result.highWater,
lowWater: result.lowWater,
location: result.location
location: result.location,
role: getRoleForLocationSource(fetchLocation.source)
})
setModal('tides')
} catch (err) {
@@ -892,7 +888,8 @@ export default function LiveLogView({
return
}
if (err.code === 'PLACE_NOT_FOUND') {
void showAlert(t('logs.tide_place_not_found', { place: departure.trim() }), t('logs.tides'))
const query = fetchLocation.mode === 'by-place' ? fetchLocation.query : ''
void showAlert(t('logs.tide_place_not_found', { place: query.trim() }), t('logs.tides'))
return
}
if (err.code === 'NO_DATA_FOR_DATE') {
@@ -912,11 +909,38 @@ export default function LiveLogView({
})()
}
const handleFetchTides = () => {
if (!entryId || busy || tidesLoading) return
if (!isOnline) {
void showAlert(t('logs.weather_offline'), t('logs.tides'))
return
}
const available = getAvailableTideLocations({
departure,
destination,
events,
entryDate: date
})
if (available.length === 0) {
void showAlert(t('logs.tide_location_required'), t('logs.tides'))
return
}
if (available.length === 1) {
startTideFetchForLocation(available[0].fetchLocation)
} else {
setTideLocationPickerOptions(available)
setModal('tides_picker')
}
}
const confirmTides = () => {
if (!entryId || !tidePreview || busy) return
const preview = tidePreview
void runQuickAction(async () => {
await patchEntryTides(logbookId, entryId, {
await patchEntryTides(logbookId, entryId, preview.role, {
highWater: preview.highWater,
lowWater: preview.lowWater,
...preview.location
@@ -1619,6 +1643,24 @@ export default function LiveLogView({
/>
) : null}
{modal === 'tides_picker' && tideLocationPickerOptions ? (
<TideLocationPickerModal
title={t('logs.tide_location_picker_title')}
hint={t('logs.tide_location_picker_hint')}
cancelLabel={t('logs.live_cancel')}
options={tideLocationPickerOptions}
onCancel={() => {
setTideLocationPickerOptions(null)
closeModal()
}}
onSelect={(option) => {
setTideLocationPickerOptions(null)
closeModal()
startTideFetchForLocation(option.fetchLocation)
}}
/>
) : null}
{modal === 'tides' && tidePreview && (
<div
className="live-log-modal-backdrop"