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:
@@ -2660,6 +2660,13 @@ html.theme-cupertino .events-scroll-container {
|
||||
color: var(--app-text-muted);
|
||||
}
|
||||
|
||||
.stats-section-subtitle {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--app-text-primary);
|
||||
}
|
||||
|
||||
.stats-route-chain {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
@@ -2762,6 +2769,14 @@ html.theme-cupertino .events-scroll-container {
|
||||
background: linear-gradient(180deg, #38bdf8, #0284c7);
|
||||
}
|
||||
|
||||
.stats-bar--motor-hours {
|
||||
background: linear-gradient(180deg, #a78bfa, #7c3aed);
|
||||
}
|
||||
|
||||
.stats-bar--fuel-per-hour {
|
||||
background: linear-gradient(180deg, #fb923c, #ea580c);
|
||||
}
|
||||
|
||||
.stats-bar-label {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BarChart2, Anchor, Droplets, Fuel, Sailboat, Gauge } from 'lucide-react'
|
||||
import { BarChart2, Anchor, Droplets, Fuel, Sailboat, Gauge, Timer } from 'lucide-react'
|
||||
import MultiTrackMap from './MultiTrackMap.tsx'
|
||||
import {
|
||||
formatLiters,
|
||||
formatHours,
|
||||
formatNm,
|
||||
loadAccountStats,
|
||||
loadLogbookStats,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
type TravelDayStats
|
||||
} from '../services/statsAggregation.js'
|
||||
import { compareTravelDaysChronological } from '../utils/logEntryTankLevels.js'
|
||||
import { formatFuelPerMotorHour } from '../utils/fuelStats.js'
|
||||
|
||||
interface StatsDashboardProps {
|
||||
logbookId: string
|
||||
@@ -78,12 +80,26 @@ function TotalsGrid({ totals }: { totals: StatsTotals }) {
|
||||
value={formatNm(totals.motorDistanceNm)}
|
||||
unit={t('stats.unit_nm')}
|
||||
/>
|
||||
<KpiCard
|
||||
icon={<Timer size={20} />}
|
||||
label={t('stats.motor_hours_total')}
|
||||
value={formatHours(totals.totalMotorHours)}
|
||||
unit={t('stats.unit_h')}
|
||||
/>
|
||||
<KpiCard
|
||||
icon={<Fuel size={20} />}
|
||||
label={t('stats.fuel_total')}
|
||||
value={formatLiters(totals.totalFuelL)}
|
||||
unit={t('stats.unit_l')}
|
||||
/>
|
||||
{totals.fuelPerMotorHourL != null && (
|
||||
<KpiCard
|
||||
icon={<Timer size={20} />}
|
||||
label={t('stats.fuel_per_motor_hour')}
|
||||
value={formatFuelPerMotorHour(totals.fuelPerMotorHourL)}
|
||||
unit={`${t('stats.unit_l')}/${t('stats.unit_h')}`}
|
||||
/>
|
||||
)}
|
||||
<KpiCard
|
||||
icon={<Droplets size={20} />}
|
||||
label={t('stats.water_total')}
|
||||
@@ -247,6 +263,36 @@ function LogbookScopeView({ summary }: { summary: LogbookStatsSummary }) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="member-editor-card glass mt-6">
|
||||
<h3 className="stats-section-title">{t('stats.daily_motor_hours')}</h3>
|
||||
<p className="stats-section-sub">
|
||||
{t('stats.avg_motor_hours')}: {formatHours(totals.avgMotorHoursPerDay)} {t('stats.unit_h')}
|
||||
{totals.fuelPerMotorHourL != null && (
|
||||
<>
|
||||
{' · '}
|
||||
{t('stats.fuel_per_motor_hour')}: {formatFuelPerMotorHour(totals.fuelPerMotorHourL)} {t('stats.unit_l')}/{t('stats.unit_h')}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<DailyBarChart
|
||||
days={travelDays}
|
||||
valueFn={(d) => d.motorHours}
|
||||
barClass="stats-bar--motor-hours"
|
||||
formatValue={formatHours}
|
||||
/>
|
||||
{travelDays.some((d) => d.fuelPerMotorHourL != null) && (
|
||||
<>
|
||||
<h4 className="stats-section-subtitle mt-4">{t('stats.daily_fuel_per_motor_hour')}</h4>
|
||||
<DailyBarChart
|
||||
days={travelDays}
|
||||
valueFn={(d) => d.fuelPerMotorHourL ?? 0}
|
||||
barClass="stats-bar--fuel-per-hour"
|
||||
formatValue={(v) => formatFuelPerMotorHour(v > 0 ? v : null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="member-editor-card glass mt-6">
|
||||
<h3 className="stats-section-title">{t('stats.daily_consumption')}</h3>
|
||||
<p className="stats-section-sub">
|
||||
@@ -256,6 +302,9 @@ function LogbookScopeView({ summary }: { summary: LogbookStatsSummary }) {
|
||||
{totals.fuelPerNmL != null && (
|
||||
<> · {t('stats.fuel_per_nm')}: {totals.fuelPerNmL} {t('stats.unit_l')}/{t('stats.unit_nm')}</>
|
||||
)}
|
||||
{totals.fuelPerMotorHourL != null && (
|
||||
<> · {t('stats.fuel_per_motor_hour')}: {formatFuelPerMotorHour(totals.fuelPerMotorHourL)} {t('stats.unit_l')}/{t('stats.unit_h')}</>
|
||||
)}
|
||||
</p>
|
||||
<ConsumptionChart days={travelDays} />
|
||||
</div>
|
||||
@@ -367,6 +416,7 @@ export default function StatsDashboard({ logbookId, logbookTitle }: StatsDashboa
|
||||
<th>{t('stats.travel_days')}</th>
|
||||
<th>{t('stats.total_distance')}</th>
|
||||
<th>{t('stats.fuel_total')}</th>
|
||||
<th>{t('stats.motor_hours_total')}</th>
|
||||
<th>{t('stats.water_total')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -377,6 +427,7 @@ export default function StatsDashboard({ logbookId, logbookTitle }: StatsDashboa
|
||||
<td>{lb.totals.travelDayCount}</td>
|
||||
<td>{formatNm(lb.totals.totalDistanceNm)} {t('stats.unit_nm')}</td>
|
||||
<td>{formatLiters(lb.totals.totalFuelL)} {t('stats.unit_l')}</td>
|
||||
<td>{formatHours(lb.totals.totalMotorHours)} {t('stats.unit_h')}</td>
|
||||
<td>{formatLiters(lb.totals.totalFreshwaterL)} {t('stats.unit_l')}</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -397,8 +448,39 @@ export default function StatsDashboard({ logbookId, logbookTitle }: StatsDashboa
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="member-editor-card glass mt-6">
|
||||
<h3 className="stats-section-title">{t('stats.daily_motor_hours')}</h3>
|
||||
{accountStats.totals.fuelPerMotorHourL != null && (
|
||||
<p className="stats-section-sub">
|
||||
{t('stats.fuel_per_motor_hour')}: {formatFuelPerMotorHour(accountStats.totals.fuelPerMotorHourL)} {t('stats.unit_l')}/{t('stats.unit_h')}
|
||||
</p>
|
||||
)}
|
||||
<DailyBarChart
|
||||
days={allAccountDays}
|
||||
valueFn={(d) => d.motorHours}
|
||||
barClass="stats-bar--motor-hours"
|
||||
formatValue={formatHours}
|
||||
/>
|
||||
{allAccountDays.some((d) => d.fuelPerMotorHourL != null) && (
|
||||
<>
|
||||
<h4 className="stats-section-subtitle mt-4">{t('stats.daily_fuel_per_motor_hour')}</h4>
|
||||
<DailyBarChart
|
||||
days={allAccountDays}
|
||||
valueFn={(d) => d.fuelPerMotorHourL ?? 0}
|
||||
barClass="stats-bar--fuel-per-hour"
|
||||
formatValue={(v) => formatFuelPerMotorHour(v > 0 ? v : null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="member-editor-card glass mt-6">
|
||||
<h3 className="stats-section-title">{t('stats.daily_consumption')}</h3>
|
||||
{accountStats.totals.fuelPerMotorHourL != null && (
|
||||
<p className="stats-section-sub">
|
||||
{t('stats.fuel_per_motor_hour')}: {formatFuelPerMotorHour(accountStats.totals.fuelPerMotorHourL)} {t('stats.unit_l')}/{t('stats.unit_h')}
|
||||
</p>
|
||||
)}
|
||||
<ConsumptionChart days={allAccountDays} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -205,6 +205,8 @@
|
||||
"event_heel": "Krängung (°)",
|
||||
"event_sails": "Segelführung / Motor",
|
||||
"motor_propulsion": "Maschinenfahrt",
|
||||
"motor_hours": "Maschinenstunden (gesamt)",
|
||||
"fuel_per_motor_hour": "Verbrauch pro Maschinenstunde",
|
||||
"event_distance": "Distanz (sm)",
|
||||
"export_csv": "CSV herunterladen",
|
||||
"share_csv": "CSV teilen",
|
||||
@@ -479,6 +481,9 @@
|
||||
"travel_days": "Reisetage",
|
||||
"sail_distance": "Unter Segel",
|
||||
"motor_distance": "Maschinenfahrt",
|
||||
"motor_hours_total": "Maschinenstunden gesamt",
|
||||
"daily_motor_hours": "Maschinenstunden pro Reisetag",
|
||||
"avg_motor_hours": "Ø Maschinenstunden pro Reisetag",
|
||||
"unknown_propulsion": "Unbekannt",
|
||||
"fuel_total": "Kraftstoff gesamt",
|
||||
"water_total": "Wasser gesamt",
|
||||
@@ -492,9 +497,12 @@
|
||||
"avg_fuel": "Ø Kraftstoff",
|
||||
"avg_water": "Ø Wasser",
|
||||
"fuel_per_nm": "Kraftstoff pro sm",
|
||||
"fuel_per_motor_hour": "Kraftstoff pro Maschinenstunde",
|
||||
"daily_fuel_per_motor_hour": "Kraftstoffverbrauch pro Maschinenstunde je Reisetag",
|
||||
"fuel_legend": "Kraftstoff",
|
||||
"water_legend": "Wasser",
|
||||
"unit_nm": "sm",
|
||||
"unit_h": "h",
|
||||
"unit_l": "L",
|
||||
"day_label": "Tag {{day}}",
|
||||
"account_logbooks": "Logbücher im Überblick",
|
||||
|
||||
@@ -205,6 +205,8 @@
|
||||
"event_heel": "Heel Angle (°)",
|
||||
"event_sails": "Sails / Motor Status",
|
||||
"motor_propulsion": "Engine Propulsion",
|
||||
"motor_hours": "Engine hours (total)",
|
||||
"fuel_per_motor_hour": "Consumption per engine hour",
|
||||
"event_distance": "Distance (nm)",
|
||||
"export_csv": "Download CSV",
|
||||
"share_csv": "Share CSV",
|
||||
@@ -479,6 +481,9 @@
|
||||
"travel_days": "Travel days",
|
||||
"sail_distance": "Under sail",
|
||||
"motor_distance": "Engine",
|
||||
"motor_hours_total": "Total engine hours",
|
||||
"daily_motor_hours": "Engine hours per travel day",
|
||||
"avg_motor_hours": "Avg. engine hours per travel day",
|
||||
"unknown_propulsion": "Unknown",
|
||||
"fuel_total": "Total fuel",
|
||||
"water_total": "Total water",
|
||||
@@ -492,9 +497,12 @@
|
||||
"avg_fuel": "Avg. fuel",
|
||||
"avg_water": "Avg. water",
|
||||
"fuel_per_nm": "Fuel per nm",
|
||||
"fuel_per_motor_hour": "Fuel per engine hour",
|
||||
"daily_fuel_per_motor_hour": "Fuel consumption per engine hour by travel day",
|
||||
"fuel_legend": "Fuel",
|
||||
"water_legend": "Water",
|
||||
"unit_nm": "nm",
|
||||
"unit_h": "h",
|
||||
"unit_l": "L",
|
||||
"day_label": "Day {{day}}",
|
||||
"account_logbooks": "Logbooks overview",
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
|
||||
const headers = [
|
||||
'Date', 'Day of Travel', 'Departure Port', 'Destination Port',
|
||||
'Skipper Signature', 'Crew Signature',
|
||||
'Track Distance (nm)', 'Track Max Speed (kn)', 'Track Avg Speed (kn)',
|
||||
'Track Distance (nm)', 'Track Max Speed (kn)', 'Track Avg Speed (kn)', 'Motor Hours (h)',
|
||||
'Event Time', 'MgK Course', 'RwK Course',
|
||||
'Wind Dir', 'Wind Str', 'Barometer (hPa)', 'Sea State',
|
||||
'Current', 'Heel Angle', 'Sails/Motor', 'Log (nm)', 'Distance (nm)',
|
||||
@@ -113,6 +113,7 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
|
||||
const trackDist = entry.trackDistanceNm ?? '';
|
||||
const trackMax = entry.trackSpeedMaxKn ?? '';
|
||||
const trackAvg = entry.trackSpeedAvgKn ?? '';
|
||||
const motorH = entry.motorHours ?? '';
|
||||
const fwM = entry.freshwater?.morning ?? '';
|
||||
const fwR = entry.freshwater?.refilled ?? '';
|
||||
const fwE = entry.freshwater?.evening ?? '';
|
||||
@@ -128,7 +129,7 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
|
||||
rows.push([
|
||||
dateVal, travelDay, dep, dest,
|
||||
signS, signC,
|
||||
trackDist, trackMax, trackAvg,
|
||||
trackDist, trackMax, trackAvg, motorH,
|
||||
'', '', '',
|
||||
'', '', '', '',
|
||||
'', '', '', '', '',
|
||||
@@ -144,7 +145,7 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
|
||||
rows.push([
|
||||
dateVal, travelDay, dep, dest,
|
||||
signS, signC,
|
||||
trackDist, trackMax, trackAvg,
|
||||
trackDist, trackMax, trackAvg, motorH,
|
||||
ev.time || '', ev.mgk || '', ev.rwk || '',
|
||||
ev.windDirection || '', ev.windStrength || '', ev.windPressure || '', ev.seaState || '',
|
||||
ev.current || '', ev.heel || '', ev.sailsOrMotor || '', ev.logReading || '', ev.distance || '',
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface DemoDaySpec {
|
||||
filename: string
|
||||
freshwater: { morning: number; refilled: number; evening: number; consumption: number }
|
||||
fuel: { morning: number; refilled: number; evening: number; consumption: number }
|
||||
motorHours?: number
|
||||
events: Array<Record<string, string>>
|
||||
}
|
||||
|
||||
@@ -100,6 +101,7 @@ export function buildDemoDays(): DemoDaySpec[] {
|
||||
filename: 'laboe-damp.gpx',
|
||||
freshwater: { morning: 105, refilled: 25, evening: 110, consumption: 20 },
|
||||
fuel: { morning: 78, refilled: 0, evening: 70, consumption: 8 },
|
||||
motorHours: 1.5,
|
||||
events: [
|
||||
{
|
||||
time: '09:00',
|
||||
@@ -247,6 +249,9 @@ export function buildPublicDemoFixture(): PublicDemoFixture {
|
||||
entryPayload.trackSpeedMaxKn = stats.speedMaxKn
|
||||
entryPayload.trackSpeedAvgKn = stats.speedAvgKn
|
||||
}
|
||||
if (day.motorHours != null && day.motorHours > 0) {
|
||||
entryPayload.motorHours = day.motorHours
|
||||
}
|
||||
|
||||
entries.push(entryPayload as PublicDemoFixture['entries'][number])
|
||||
|
||||
@@ -303,6 +308,9 @@ export function buildDemoEntryPayloads(): Array<{
|
||||
entryPayload.trackSpeedMaxKn = stats.speedMaxKn
|
||||
entryPayload.trackSpeedAvgKn = stats.speedAvgKn
|
||||
}
|
||||
if (day.motorHours != null && day.motorHours > 0) {
|
||||
entryPayload.motorHours = day.motorHours
|
||||
}
|
||||
|
||||
return {
|
||||
entryId,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
parseEventDistanceNm,
|
||||
splitDistanceByPropulsion
|
||||
} from '../utils/propulsionStats.js'
|
||||
import { computeFuelPerMotorHour } from '../utils/fuelStats.js'
|
||||
|
||||
export type DistanceSource = 'gps' | 'events' | 'none'
|
||||
|
||||
@@ -27,6 +28,8 @@ export interface TravelDayStats {
|
||||
sailDistanceNm: number
|
||||
motorDistanceNm: number
|
||||
unknownPropulsionNm: number
|
||||
motorHours: number
|
||||
fuelPerMotorHourL: number | null
|
||||
hasGpsTrack: boolean
|
||||
}
|
||||
|
||||
@@ -59,12 +62,15 @@ export interface StatsTotals {
|
||||
sailDistanceNm: number
|
||||
motorDistanceNm: number
|
||||
unknownPropulsionNm: number
|
||||
totalMotorHours: number
|
||||
totalFuelL: number
|
||||
totalFreshwaterL: number
|
||||
avgDistancePerDayNm: number
|
||||
avgMotorHoursPerDay: number
|
||||
avgFuelPerDayL: number
|
||||
avgFreshwaterPerDayL: number
|
||||
fuelPerNmL: number | null
|
||||
fuelPerMotorHourL: number | null
|
||||
}
|
||||
|
||||
const TRACK_COLORS = [
|
||||
@@ -102,6 +108,7 @@ function buildTotals(days: TravelDayStats[]): StatsTotals {
|
||||
const sailDistanceNm = days.reduce((sum, d) => sum + d.sailDistanceNm, 0)
|
||||
const motorDistanceNm = days.reduce((sum, d) => sum + d.motorDistanceNm, 0)
|
||||
const unknownPropulsionNm = days.reduce((sum, d) => sum + d.unknownPropulsionNm, 0)
|
||||
const totalMotorHours = days.reduce((sum, d) => sum + d.motorHours, 0)
|
||||
const totalFuelL = days.reduce((sum, d) => sum + d.fuelConsumptionL, 0)
|
||||
const totalFreshwaterL = days.reduce((sum, d) => sum + d.freshwaterConsumptionL, 0)
|
||||
|
||||
@@ -112,10 +119,13 @@ function buildTotals(days: TravelDayStats[]): StatsTotals {
|
||||
sailDistanceNm: Number(sailDistanceNm.toFixed(2)),
|
||||
motorDistanceNm: Number(motorDistanceNm.toFixed(2)),
|
||||
unknownPropulsionNm: Number(unknownPropulsionNm.toFixed(2)),
|
||||
totalMotorHours: Number(totalMotorHours.toFixed(1)),
|
||||
totalFuelL: Number(totalFuelL.toFixed(1)),
|
||||
totalFreshwaterL: Number(totalFreshwaterL.toFixed(1)),
|
||||
avgDistancePerDayNm:
|
||||
travelDayCount > 0 ? Number((totalDistanceNm / travelDayCount).toFixed(2)) : 0,
|
||||
avgMotorHoursPerDay:
|
||||
travelDayCount > 0 ? Number((totalMotorHours / travelDayCount).toFixed(1)) : 0,
|
||||
avgFuelPerDayL:
|
||||
travelDayCount > 0 ? Number((totalFuelL / travelDayCount).toFixed(1)) : 0,
|
||||
avgFreshwaterPerDayL:
|
||||
@@ -123,7 +133,8 @@ function buildTotals(days: TravelDayStats[]): StatsTotals {
|
||||
fuelPerNmL:
|
||||
totalDistanceNm > 0 && totalFuelL > 0
|
||||
? Number((totalFuelL / totalDistanceNm).toFixed(2))
|
||||
: null
|
||||
: null,
|
||||
fuelPerMotorHourL: computeFuelPerMotorHour(totalFuelL, totalMotorHours)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +191,9 @@ async function loadTravelDaysForLogbook(
|
||||
hasGpsTrack = !!(await db.gpsTracks.get(entry.payloadId))
|
||||
}
|
||||
|
||||
const fuelConsumptionL = Number(payload.fuel?.consumption) || 0
|
||||
const motorHours = Number(payload.motorHours) || 0
|
||||
|
||||
days.push({
|
||||
entryId: entry.payloadId,
|
||||
logbookId,
|
||||
@@ -189,11 +203,13 @@ async function loadTravelDaysForLogbook(
|
||||
destination: payload.destination || '',
|
||||
distanceNm,
|
||||
distanceSource,
|
||||
fuelConsumptionL: Number(payload.fuel?.consumption) || 0,
|
||||
fuelConsumptionL,
|
||||
freshwaterConsumptionL: Number(payload.freshwater?.consumption) || 0,
|
||||
sailDistanceNm: propulsion.sailDistanceNm,
|
||||
motorDistanceNm: propulsion.motorDistanceNm,
|
||||
unknownPropulsionNm: propulsion.unknownPropulsionNm,
|
||||
motorHours,
|
||||
fuelPerMotorHourL: computeFuelPerMotorHour(fuelConsumptionL, motorHours),
|
||||
hasGpsTrack
|
||||
})
|
||||
}
|
||||
@@ -249,3 +265,7 @@ export function formatNm(value: number): string {
|
||||
export function formatLiters(value: number): string {
|
||||
return Number.isInteger(value) ? String(value) : value.toFixed(1)
|
||||
}
|
||||
|
||||
export function formatHours(value: number): string {
|
||||
return Number.isInteger(value) ? String(value) : value.toFixed(1)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/** Liters per motor hour from daily fuel consumption and motor hours. */
|
||||
export function computeFuelPerMotorHour(
|
||||
fuelConsumptionL: number,
|
||||
motorHours: number
|
||||
): number | null {
|
||||
if (motorHours <= 0) return null
|
||||
return Number((fuelConsumptionL / motorHours).toFixed(2))
|
||||
}
|
||||
|
||||
export function formatFuelPerMotorHour(value: number | null | undefined): string {
|
||||
if (value == null) return '—'
|
||||
return Number.isInteger(value) ? String(value) : value.toFixed(2)
|
||||
}
|
||||
@@ -71,6 +71,7 @@ export interface LogEntryPayloadInput {
|
||||
trackDistanceNm?: number
|
||||
trackSpeedMaxKn?: number
|
||||
trackSpeedAvgKn?: number
|
||||
motorHours?: number
|
||||
events: LogEventPayload[]
|
||||
}
|
||||
|
||||
@@ -88,6 +89,9 @@ export function buildLogEntryPayload(input: LogEntryPayloadInput): Record<string
|
||||
if (input.trackDistanceNm !== undefined) payload.trackDistanceNm = input.trackDistanceNm
|
||||
if (input.trackSpeedMaxKn !== undefined) payload.trackSpeedMaxKn = input.trackSpeedMaxKn
|
||||
if (input.trackSpeedAvgKn !== undefined) payload.trackSpeedAvgKn = input.trackSpeedAvgKn
|
||||
if (input.motorHours !== undefined && input.motorHours > 0) {
|
||||
payload.motorHours = Number(input.motorHours.toFixed(2))
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user