feat(logs): Maschinenstunden pro Reisetag und Verbrauch pro Stunde

Maschinenstunden sind im Journal erfassbar; der Kraftstoffverbrauch pro Maschinenstunde wird aus Tagesverbrauch und Maschinenstunden berechnet und in Journal sowie Statistik als Read-only angezeigt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-30 19:29:38 +02:00
co-authored by Cursor
parent 4acb9b1290
commit d231a7fb40
10 changed files with 216 additions and 7 deletions
+51 -1
View File
@@ -37,6 +37,7 @@ import {
type SavedTrack
} from '../services/trackUpload.js'
import { computeTrackStats, formatTrackStats } from '../utils/trackStats.js'
import { computeFuelPerMotorHour, formatFuelPerMotorHour } from '../utils/fuelStats.js'
import { useRegisterUnsavedChanges } from '../context/UnsavedChangesContext.tsx'
function emptyTankLevels() {
@@ -49,6 +50,7 @@ function fingerprintFromStoredEntry(decrypted: Record<string, unknown>): string
const trackDistance = decrypted.trackDistanceNm
const trackSpeedMax = decrypted.trackSpeedMaxKn
const trackSpeedAvg = decrypted.trackSpeedAvgKn
const motorHoursRaw = decrypted.motorHours
const payload = buildLogEntryPayload({
date: String(decrypted.date || ''),
@@ -79,6 +81,10 @@ function fingerprintFromStoredEntry(decrypted: Record<string, unknown>): string
trackSpeedAvg != null && trackSpeedAvg !== ''
? parseFloat(String(trackSpeedAvg))
: undefined,
motorHours:
motorHoursRaw != null && motorHoursRaw !== ''
? parseFloat(String(motorHoursRaw))
: undefined,
events: (decrypted.events as LogEventPayload[]) || []
})
@@ -149,6 +155,9 @@ export default function LogEntryEditor({
const [trackSpeedMaxKn, setTrackSpeedMaxKn] = useState('')
const [trackSpeedAvgKn, setTrackSpeedAvgKn] = useState('')
// Motor hours under engine propulsion (per travel day)
const [motorHours, setMotorHours] = useState('')
// Events list state
const [events, setEvents] = useState<LogEvent[]>([])
@@ -209,6 +218,11 @@ export default function LogEntryEditor({
if (entry?.trackSpeedAvgKn != null && entry.trackSpeedAvgKn !== '') {
setTrackSpeedAvgKn(String(entry.trackSpeedAvgKn))
}
if (entry?.motorHours != null && entry.motorHours !== '') {
setMotorHours(String(entry.motorHours))
} else {
setMotorHours('')
}
}
const buildPayloadForSigning = useCallback((eventsOverride?: LogEvent[]) => {
@@ -232,16 +246,22 @@ export default function LogEntryEditor({
trackDistanceNm: trackDistanceNm.trim() ? parseFloat(trackDistanceNm) : undefined,
trackSpeedMaxKn: trackSpeedMaxKn.trim() ? parseFloat(trackSpeedMaxKn) : undefined,
trackSpeedAvgKn: trackSpeedAvgKn.trim() ? parseFloat(trackSpeedAvgKn) : undefined,
motorHours: motorHours.trim() ? parseFloat(motorHours) : undefined,
events: eventsOverride ?? events
})
}, [
date, dayOfTravel, departure, destination,
fwMorning, fwRefilled, fwEvening, fwConsumption,
fuelMorning, fuelRefilled, fuelEvening, fuelConsumption,
trackDistanceNm, trackSpeedMaxKn, trackSpeedAvgKn,
trackDistanceNm, trackSpeedMaxKn, trackSpeedAvgKn, motorHours,
events
])
const fuelPerMotorHour = useMemo(
() => computeFuelPerMotorHour(parseFloat(fuelConsumption) || 0, parseFloat(motorHours) || 0),
[fuelConsumption, motorHours]
)
const currentFingerprint = useMemo(() => {
const payload = buildPayloadForSigning()
return JSON.stringify({
@@ -1109,6 +1129,20 @@ export default function LogEntryEditor({
disabled={saving || readOnly}
/>
</div>
<div className="input-group">
<label>{t('logs.motor_hours')}</label>
<input
type="number"
className="input-text"
value={motorHours}
onChange={(e) => setMotorHours(e.target.value)}
disabled={saving || readOnly}
min="0"
step="0.1"
placeholder="0"
/>
</div>
</div>
</div>
@@ -1219,6 +1253,22 @@ export default function LogEntryEditor({
aria-readonly="true"
/>
</div>
<div className="input-group">
<label>{t('logs.fuel_per_motor_hour')}</label>
<input
type="text"
className="input-text consumption-value"
value={
fuelPerMotorHour != null
? `${formatFuelPerMotorHour(fuelPerMotorHour)} L/h`
: '—'
}
readOnly
tabIndex={-1}
aria-readonly="true"
/>
</div>
</div>
</div>
</div>