import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { BarChart2, Anchor, Droplets, Fuel, Sailboat, Gauge, Timer } from 'lucide-react' import MultiTrackMap from './MultiTrackMap.tsx' import { formatLiters, formatHours, formatNm, loadAccountStats, loadLogbookStats, type LogbookStatsSummary, type StatsTotals, type TravelDayStats } from '../services/statsAggregation.js' import { compareTravelDaysChronological } from '../utils/logEntryTankLevels.js' import { formatFuelPerMotorHour } from '../utils/fuelStats.js' import { formatAppDecimal } from '../utils/numberFormat.js' import { loadLogbookEventSeries, type EventSeriesPoint, type EventSeriesSummary } from '../services/eventSeriesAggregation.js' interface StatsDashboardProps { logbookId: string logbookTitle: string } type StatsScope = 'logbook' | 'account' function maxBarValue(days: TravelDayStats[], pick: (d: TravelDayStats) => number): number { if (days.length === 0) return 1 return Math.max(1, ...days.map(pick)) } function KpiCard({ icon, label, value, unit }: { icon: ReactNode label: string value: string unit?: string }) { return (
{icon}
{label} {value} {unit ? {unit} : null}
) } function TotalsGrid({ totals }: { totals: StatsTotals }) { const { t } = useTranslation() return (
} label={t('stats.total_distance')} value={formatNm(totals.totalDistanceNm)} unit={t('stats.unit_nm')} /> } label={t('stats.travel_days')} value={String(totals.travelDayCount)} /> } label={t('stats.sail_distance')} value={formatNm(totals.sailDistanceNm)} unit={t('stats.unit_nm')} /> } label={t('stats.motor_distance')} value={formatNm(totals.motorDistanceNm)} unit={t('stats.unit_nm')} /> } label={t('stats.motor_hours_total')} value={formatHours(totals.totalMotorHours)} unit={t('stats.unit_h')} /> } label={t('stats.fuel_total')} value={formatLiters(totals.totalFuelL)} unit={t('stats.unit_l')} /> {totals.fuelPerMotorHourL != null && ( } label={t('stats.fuel_per_motor_hour')} value={formatFuelPerMotorHour(totals.fuelPerMotorHourL)} unit={`${t('stats.unit_l')}/${t('stats.unit_h')}`} /> )} } label={t('stats.water_total')} value={formatLiters(totals.totalFreshwaterL)} unit={t('stats.unit_l')} />
) } function DailyBarChart({ days, valueFn, barClass, formatValue }: { days: TravelDayStats[] valueFn: (d: TravelDayStats) => number barClass: string formatValue: (v: number) => string }) { const { t } = useTranslation() const max = maxBarValue(days, valueFn) return (
{days.map((day) => { const value = valueFn(day) const heightPct = max > 0 ? Math.max(2, (value / max) * 100) : 0 const label = day.date ? new Date(day.date).toLocaleDateString(undefined, { day: '2-digit', month: '2-digit' }) : t('stats.day_label', { day: day.dayOfTravel }) return (
{value > 0 ? formatValue(value) : ''}
{label} {t('stats.day_label', { day: day.dayOfTravel })}
) })}
) } function ConsumptionChart({ days }: { days: TravelDayStats[] }) { const { t } = useTranslation() const max = maxBarValue(days, (d) => Math.max(d.fuelConsumptionL, d.freshwaterConsumptionL)) return (
{days.map((day) => { const fuelH = max > 0 ? Math.max(2, (day.fuelConsumptionL / max) * 100) : 0 const waterH = max > 0 ? Math.max(2, (day.freshwaterConsumptionL / max) * 100) : 0 const label = day.date ? new Date(day.date).toLocaleDateString(undefined, { day: '2-digit', month: '2-digit' }) : t('stats.day_label', { day: day.dayOfTravel }) return (
{label}
) })}
{t('stats.fuel_legend')} {t('stats.water_legend')}
) } function PropulsionBreakdown({ totals }: { totals: StatsTotals }) { const { t } = useTranslation() const total = totals.sailDistanceNm + totals.motorDistanceNm + totals.unknownPropulsionNm if (total <= 0) return null const sailPct = (totals.sailDistanceNm / total) * 100 const motorPct = (totals.motorDistanceNm / total) * 100 const unknownPct = (totals.unknownPropulsionNm / total) * 100 return (
{totals.sailDistanceNm > 0 && (
)} {totals.motorDistanceNm > 0 && (
)} {totals.unknownPropulsionNm > 0 && (
)}
{t('stats.sail_distance')}: {formatNm(totals.sailDistanceNm)} {t('stats.unit_nm')} ({formatAppDecimal(sailPct, { maximumFractionDigits: 0 })}%) {t('stats.motor_distance')}: {formatNm(totals.motorDistanceNm)} {t('stats.unit_nm')} ({formatAppDecimal(motorPct, { maximumFractionDigits: 0 })}%) {totals.unknownPropulsionNm > 0 && ( {t('stats.unknown_propulsion')}: {formatNm(totals.unknownPropulsionNm)} {t('stats.unit_nm')} )}

{t('stats.propulsion_hint')}

) } function EventSeriesList({ title, points, emptyLabel }: { title: string; points: EventSeriesPoint[]; emptyLabel: string }) { if (points.length === 0) { return (

{title}

{emptyLabel}

) } return (

{title}

    {points.map((point, idx) => (
  • {new Date(point.date).toLocaleDateString(undefined, { day: '2-digit', month: '2-digit' })} {' · '} {point.time} {point.summary}
  • ))}
) } function EventSeriesPanel({ series }: { series: EventSeriesSummary }) { const { t } = useTranslation() const motorPoints = series.motor.map((point) => ({ ...point, summary: point.summary === 'start' ? t('logs.live_motor_start') : t('logs.live_motor_stop') })) return (

{t('stats.event_series_title')}

{t('stats.event_series_hint')}

) } function LogbookScopeView({ summary, eventSeries }: { summary: LogbookStatsSummary eventSeries: EventSeriesSummary | null }) { const { t } = useTranslation() const { travelDays, routePorts, trackSegments, totals } = summary if (travelDays.length === 0) { return
{t('stats.no_data')}
} return ( <> {routePorts.length > 0 && (

{t('stats.route_overview')}

{routePorts.map((port, idx) => ( {idx > 0 && } {port} ))}

)} {trackSegments.length > 0 && (

{t('stats.route_map_title')}

)}

{t('stats.daily_etmal')}

{t('stats.avg_distance')}: {formatNm(totals.avgDistancePerDayNm)} {t('stats.unit_nm')}

d.distanceNm} barClass="stats-bar--distance" formatValue={formatNm} />

{t('stats.daily_motor_hours')}

{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')} )}

d.motorHours} barClass="stats-bar--motor-hours" formatValue={formatHours} /> {travelDays.some((d) => d.fuelPerMotorHourL != null) && ( <>

{t('stats.daily_fuel_per_motor_hour')}

d.fuelPerMotorHourL ?? 0} barClass="stats-bar--fuel-per-hour" formatValue={(v) => formatFuelPerMotorHour(v > 0 ? v : null)} /> )}

{t('stats.daily_consumption')}

{t('stats.avg_fuel')}: {formatLiters(totals.avgFuelPerDayL)} {t('stats.unit_l')} {' · '} {t('stats.avg_water')}: {formatLiters(totals.avgFreshwaterPerDayL)} {t('stats.unit_l')} {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')} )}

{t('stats.propulsion_title')}

{eventSeries && } ) } export default function StatsDashboard({ logbookId, logbookTitle }: StatsDashboardProps) { const { t } = useTranslation() const [scope, setScope] = useState('logbook') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [logbookStats, setLogbookStats] = useState(null) const [eventSeries, setEventSeries] = useState(null) const [accountStats, setAccountStats] = useState> | null>(null) const loadData = useCallback(async () => { setLoading(true) setError(null) try { const [lb, acc, series] = await Promise.all([ loadLogbookStats(logbookId, logbookTitle, true), loadAccountStats(false), loadLogbookEventSeries(logbookId) ]) setLogbookStats(lb) setAccountStats(acc) setEventSeries(series) } catch (err: unknown) { console.error('Failed to load statistics:', err) setError(err instanceof Error ? err.message : 'Failed to load statistics.') } finally { setLoading(false) } }, [logbookId, logbookTitle]) useEffect(() => { void loadData() }, [loadData]) const accountLogbooksWithDays = useMemo( () => accountStats?.logbooks.filter((lb) => lb.travelDays.length > 0) ?? [], [accountStats] ) const allAccountDays = useMemo(() => { if (!accountStats) return [] const days = accountStats.logbooks.flatMap((lb) => lb.travelDays) return [...days].sort(compareTravelDaysChronological) }, [accountStats]) return (

{t('stats.title')}

{t('stats.subtitle')}

{error &&
{error}
} {loading ? (

{t('stats.loading')}

) : scope === 'logbook' && logbookStats ? ( ) : scope === 'account' && accountStats ? ( <> {accountLogbooksWithDays.length === 0 ? (
{t('stats.no_data')}
) : ( <>

{t('stats.account_logbooks')}

{accountLogbooksWithDays.map((lb) => ( ))}
{t('stats.col_logbook')} {t('stats.travel_days')} {t('stats.total_distance')} {t('stats.fuel_total')} {t('stats.motor_hours_total')} {t('stats.water_total')}
{lb.title} {lb.totals.travelDayCount} {formatNm(lb.totals.totalDistanceNm)} {t('stats.unit_nm')} {formatLiters(lb.totals.totalFuelL)} {t('stats.unit_l')} {formatHours(lb.totals.totalMotorHours)} {t('stats.unit_h')} {formatLiters(lb.totals.totalFreshwaterL)} {t('stats.unit_l')}
{accountStats.totals.travelDayCount > 0 && ( <>

{t('stats.daily_etmal')}

d.distanceNm} barClass="stats-bar--distance" formatValue={formatNm} />

{t('stats.daily_motor_hours')}

{accountStats.totals.fuelPerMotorHourL != null && (

{t('stats.fuel_per_motor_hour')}: {formatFuelPerMotorHour(accountStats.totals.fuelPerMotorHourL)} {t('stats.unit_l')}/{t('stats.unit_h')}

)} d.motorHours} barClass="stats-bar--motor-hours" formatValue={formatHours} /> {allAccountDays.some((d) => d.fuelPerMotorHourL != null) && ( <>

{t('stats.daily_fuel_per_motor_hour')}

d.fuelPerMotorHourL ?? 0} barClass="stats-bar--fuel-per-hour" formatValue={(v) => formatFuelPerMotorHour(v > 0 ? v : null)} /> )}

{t('stats.daily_consumption')}

{accountStats.totals.fuelPerMotorHourL != null && (

{t('stats.fuel_per_motor_hour')}: {formatFuelPerMotorHour(accountStats.totals.fuelPerMotorHourL)} {t('stats.unit_l')}/{t('stats.unit_h')}

)}

{t('stats.propulsion_title')}

)} )} ) : null}
) }