feat: dynamische Slider-Obergrenzen für Frischwasser und Treibstoff

Nachgefüllt ist auf Restkapazität nach Morgen begrenzt, Stand abends auf Morgen plus Nachgefüllt; Werte werden bei Änderungen automatisch gekürzt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 13:39:46 +02:00
parent 25e1bdded3
commit 23fc940324
3 changed files with 129 additions and 5 deletions
+73 -5
View File
@@ -43,7 +43,13 @@ import { computeTrackStats, formatTrackStats } from '../utils/trackStats.js'
import { computeFuelPerMotorHour, formatFuelPerMotorHour } from '../utils/fuelStats.js' import { computeFuelPerMotorHour, formatFuelPerMotorHour } from '../utils/fuelStats.js'
import { useRegisterUnsavedChanges } from '../context/UnsavedChangesContext.tsx' import { useRegisterUnsavedChanges } from '../context/UnsavedChangesContext.tsx'
import TankLiterInput from './TankLiterInput.tsx' import TankLiterInput from './TankLiterInput.tsx'
import { extractTankCapacitiesFromYacht, type VesselTankCapacities } from '../utils/tankCapacity.js' import {
computeEveningTankMaxLiters,
computeRefilledTankMaxLiters,
extractTankCapacitiesFromYacht,
formatTankLitersForInput,
type VesselTankCapacities
} from '../utils/tankCapacity.js'
function emptyTankLevels() { function emptyTankLevels() {
return { morning: 0, refilled: 0, evening: 0, consumption: 0 } return { morning: 0, refilled: 0, evening: 0, consumption: 0 }
@@ -279,6 +285,36 @@ export default function LogEntryEditor({
const tankCapacityTooltip = t('logs.tank_capacity_tooltip') const tankCapacityTooltip = t('logs.tank_capacity_tooltip')
const fwRefilledMax = useMemo(
() => computeRefilledTankMaxLiters(fwMorning, tankCapacities.freshwaterCapacityL),
[fwMorning, tankCapacities.freshwaterCapacityL]
)
const fwEveningMax = useMemo(
() =>
computeEveningTankMaxLiters(
fwMorning,
fwRefilled,
tankCapacities.freshwaterCapacityL
),
[fwMorning, fwRefilled, tankCapacities.freshwaterCapacityL]
)
const fuelRefilledMax = useMemo(
() => computeRefilledTankMaxLiters(fuelMorning, tankCapacities.fuelCapacityL),
[fuelMorning, tankCapacities.fuelCapacityL]
)
const fuelEveningMax = useMemo(
() =>
computeEveningTankMaxLiters(
fuelMorning,
fuelRefilled,
tankCapacities.fuelCapacityL
),
[fuelMorning, fuelRefilled, tankCapacities.fuelCapacityL]
)
const currentFingerprint = useMemo(() => { const currentFingerprint = useMemo(() => {
const payload = buildPayloadForSigning() const payload = buildPayloadForSigning()
return JSON.stringify({ return JSON.stringify({
@@ -538,6 +574,38 @@ export default function LogEntryEditor({
setFuelConsumption(cons >= 0 ? String(cons) : '0') setFuelConsumption(cons >= 0 ? String(cons) : '0')
}, [fuelMorning, fuelRefilled, fuelEvening]) }, [fuelMorning, fuelRefilled, fuelEvening])
useEffect(() => {
if (fwRefilledMax == null) return
const refilled = parseFloat(fwRefilled) || 0
if (refilled > fwRefilledMax) {
setFwRefilled(formatTankLitersForInput(fwRefilledMax))
}
}, [fwRefilledMax, fwMorning])
useEffect(() => {
if (fwEveningMax == null) return
const evening = parseFloat(fwEvening) || 0
if (evening > fwEveningMax) {
setFwEvening(formatTankLitersForInput(fwEveningMax))
}
}, [fwEveningMax, fwMorning, fwRefilled])
useEffect(() => {
if (fuelRefilledMax == null) return
const refilled = parseFloat(fuelRefilled) || 0
if (refilled > fuelRefilledMax) {
setFuelRefilled(formatTankLitersForInput(fuelRefilledMax))
}
}, [fuelRefilledMax, fuelMorning])
useEffect(() => {
if (fuelEveningMax == null) return
const evening = parseFloat(fuelEvening) || 0
if (evening > fuelEveningMax) {
setFuelEvening(formatTankLitersForInput(fuelEveningMax))
}
}, [fuelEveningMax, fuelMorning, fuelRefilled])
// Load yacht sails and tank capacities // Load yacht sails and tank capacities
useEffect(() => { useEffect(() => {
async function loadYachtMeta() { async function loadYachtMeta() {
@@ -1249,7 +1317,7 @@ export default function LogEntryEditor({
label={t('logs.refilled')} label={t('logs.refilled')}
value={fwRefilled} value={fwRefilled}
onChange={setFwRefilled} onChange={setFwRefilled}
maxLiters={tankCapacities.freshwaterCapacityL} maxLiters={fwRefilledMax ?? tankCapacities.freshwaterCapacityL}
disabled={saving || readOnly} disabled={saving || readOnly}
titleTooltip={tankCapacityTooltip} titleTooltip={tankCapacityTooltip}
/> />
@@ -1258,7 +1326,7 @@ export default function LogEntryEditor({
label={t('logs.evening')} label={t('logs.evening')}
value={fwEvening} value={fwEvening}
onChange={setFwEvening} onChange={setFwEvening}
maxLiters={tankCapacities.freshwaterCapacityL} maxLiters={fwEveningMax}
disabled={saving || readOnly} disabled={saving || readOnly}
titleTooltip={tankCapacityTooltip} titleTooltip={tankCapacityTooltip}
/> />
@@ -1298,7 +1366,7 @@ export default function LogEntryEditor({
label={t('logs.refilled')} label={t('logs.refilled')}
value={fuelRefilled} value={fuelRefilled}
onChange={setFuelRefilled} onChange={setFuelRefilled}
maxLiters={tankCapacities.fuelCapacityL} maxLiters={fuelRefilledMax ?? tankCapacities.fuelCapacityL}
disabled={saving || readOnly} disabled={saving || readOnly}
titleTooltip={tankCapacityTooltip} titleTooltip={tankCapacityTooltip}
/> />
@@ -1307,7 +1375,7 @@ export default function LogEntryEditor({
label={t('logs.evening')} label={t('logs.evening')}
value={fuelEvening} value={fuelEvening}
onChange={setFuelEvening} onChange={setFuelEvening}
maxLiters={tankCapacities.fuelCapacityL} maxLiters={fuelEveningMax}
disabled={saving || readOnly} disabled={saving || readOnly}
titleTooltip={tankCapacityTooltip} titleTooltip={tankCapacityTooltip}
/> />
+15
View File
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { import {
clampTankLiters, clampTankLiters,
computeEveningTankMaxLiters,
computeRefilledTankMaxLiters,
extractTankCapacitiesFromYacht, extractTankCapacitiesFromYacht,
formatTankLitersForInput, formatTankLitersForInput,
parseOptionalTankLiters, parseOptionalTankLiters,
@@ -44,4 +46,17 @@ describe('tankCapacity', () => {
expect(clampTankLiters(-5, 200)).toBe(0) expect(clampTankLiters(-5, 200)).toBe(0)
expect(clampTankLiters(50)).toBe(50) expect(clampTankLiters(50)).toBe(50)
}) })
it('computes refilled max as capacity minus morning', () => {
expect(computeRefilledTankMaxLiters('10', 60)).toBe(50)
expect(computeRefilledTankMaxLiters('60', 60)).toBeUndefined()
expect(computeRefilledTankMaxLiters('10', undefined)).toBeUndefined()
})
it('computes evening max as morning plus refilled capped by capacity', () => {
expect(computeEveningTankMaxLiters('10', '20', 60)).toBe(30)
expect(computeEveningTankMaxLiters('40', '40', 60)).toBe(60)
expect(computeEveningTankMaxLiters('10', '20')).toBe(30)
expect(computeEveningTankMaxLiters('0', '0', 60)).toBeUndefined()
})
}) })
+41
View File
@@ -50,6 +50,47 @@ export function extractTankCapacitiesFromYacht(decrypted: unknown): VesselTankCa
return capacities return capacities
} }
/** Parse a liter amount from form state (string). */
export function parseTankLitersFromInput(input: string): number {
const trimmed = input.trim().replace(',', '.')
if (!trimmed) return 0
const parsed = Number(trimmed)
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0
}
/**
* Max for refilled amount: remaining capacity after morning level.
* Returns undefined when no positive max (no slider).
*/
export function computeRefilledTankMaxLiters(
morningInput: string,
tankCapacityL?: number
): number | undefined {
if (tankCapacityL == null || tankCapacityL <= 0) return undefined
const remaining = tankCapacityL - parseTankLitersFromInput(morningInput)
if (remaining <= 0) return undefined
return remaining
}
/**
* Max for evening fill level: morning + refilled, capped by tank capacity when known.
* Returns undefined when no positive max (no slider).
*/
export function computeEveningTankMaxLiters(
morningInput: string,
refilledInput: string,
tankCapacityL?: number
): number | undefined {
const sum = parseTankLitersFromInput(morningInput) + parseTankLitersFromInput(refilledInput)
if (sum <= 0) return undefined
if (tankCapacityL != null && tankCapacityL > 0) {
return Math.min(tankCapacityL, sum)
}
return sum
}
/** Clamp numeric liter value to [0, max] when max is known. */ /** Clamp numeric liter value to [0, max] when max is known. */
export function clampTankLiters(value: number, maxLiters?: number): number { export function clampTankLiters(value: number, maxLiters?: number): number {
const clamped = Math.max(0, value) const clamped = Math.max(0, value)