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:
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user