Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/__tests__/date-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,30 @@ import { describe, expect, it } from 'vitest';
import { apiDateString, dateStringForFormat } from '$lib/domain/shared';
import { maintenanceFormSchema } from '$lib/domain/maintenance';
import { complianceFormSchema } from '$lib/domain/compliance';
import { formatDate, type FormatConfig } from '$lib/helper/date.helper';

const formatConfig: FormatConfig = {
dateFormat: 'd. MMMM yyyy',
timezone: 'UTC',
locale: 'en',
currency: 'USD',
unitOfVolume: 'liters',
unitOfLpg: 'liters',
unitOfCng: 'kg',
unitOfDistance: 'km',
mileageUnitFormat: 'km/L'
};

describe('localized date validation', () => {
it.each([
['cs', '3. září 2026'],
['ru', '3. сентября 2026']
])('formats dates with %s month names', (locale, expected) => {
expect(formatDate(new Date('2026-09-03T12:00:00Z'), { ...formatConfig, locale })).toBe(
expected
);
});

it('accepts days greater than 12 in day-first formats', () => {
expect(dateStringForFormat('dd/MM/yyyy').safeParse('13/08/2026').success).toBe(true);
expect(dateStringForFormat('dd/MM/yyyy').safeParse('31/12/2026').success).toBe(true);
Expand Down
15 changes: 7 additions & 8 deletions src/lib/components/dashboard/StackedAreaChart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@
reports_type_maintenance,
reports_type_compliance
} from '$lib/paraglide/messages/_index.js';
import { formatCurrency } from '$lib/helper/format.helper';
import { formatCurrency, formatDate } from '$lib/helper/format.helper';
import { getLocale } from '$lib/paraglide/runtime.js';

let {
data,
title,
loading = false,
bare = false
bare = false,
xAxisFormatter = (value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })
}: {
data: MonthlyExpensePoint[];
title: string;
loading?: boolean;
/** Render content only — surrounding card chrome is provided by the parent. */
bare?: boolean;
xAxisFormatter?: (_: Date) => string;
} = $props();

const SERIES = [
Expand All @@ -42,7 +45,7 @@

const chartProps = {
xAxis: {
format: (v: Date) => v.toLocaleDateString('en-IN', { month: 'short', year: '2-digit' })
format: (value: Date) => xAxisFormatter(value)
},
yAxis: {
format: (v: number) => formatCurrency(v)
Expand Down Expand Up @@ -103,11 +106,7 @@
props={chartProps}
>
{#snippet tooltip()}
<Chart.Tooltip
labelFormatter={(v: Date) =>
v.toLocaleDateString('en-IN', { month: 'long', year: 'numeric' })}
indicator="dot"
>
<Chart.Tooltip labelFormatter={formatDate} indicator="dot">
{#snippet formatter({ value, name })}
<span class="text-muted-foreground">{name}</span>
<span class="font-mono font-medium tabular-nums">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { calculateFuelAmountData } from '$stores/chart.svelte';
import { fuelLogStore } from '$stores/fuel-log.svelte';
import { vehicleStore } from '$stores/vehicle.svelte';
import { getLocale } from '$lib/paraglide/runtime.js';
import { formatDate } from '$lib/helper/format.helper';

let { loading }: { loading: boolean } = $props();

Expand Down Expand Up @@ -33,8 +35,8 @@
color="var(--chart-1)"
loading={loading || fuelLogStore.processing}
valueFormatter={(value: number) => `${value.toFixed(1)} L`}
xFormatter={(v: Date) =>
v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })}
xFormatter={(value: Date) => formatDate(value)}
xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })}
>
{#snippet filter()}
<VehicleFilterDropdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import { fuelLogStore } from '$stores/fuel-log.svelte';
import { vehicleStore } from '$stores/vehicle.svelte';
import { widget_fuel_consumption_trend } from '$lib/paraglide/messages/_index.js';
import { getLocale } from '$lib/paraglide/runtime.js';
import { formatDate } from '$lib/helper/format.helper';

// The dashboard doesn't pre-populate fuelLogStore (unlike the fuel page's
// FuelLogList) — fetch it fleet-wide here; in-flight requests are shared,
Expand All @@ -28,8 +30,8 @@
title={widget_fuel_consumption_trend()}
loading={fuelLogStore.processing}
valueFormatter={(value) => `${value.toFixed(1)} L`}
xFormatter={(v: Date) =>
v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })}
xFormatter={(value: Date) => formatDate(value)}
xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })}
>
{#snippet filter()}
<VehicleFilterDropdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import { chartStore } from '$stores/chart.svelte';
import { fuelLogStore } from '$stores/fuel-log.svelte';
import { vehicleStore } from '$stores/vehicle.svelte';
import { formatMileage } from '$lib/helper/format.helper';
import { formatDate, formatMileage } from '$lib/helper/format.helper';
import { widget_mileage_overview } from '$lib/paraglide/messages/_index.js';
import { getLocale } from '$lib/paraglide/runtime.js';

// The dashboard doesn't pre-populate fuelLogStore (unlike the fuel page's
// FuelLogList) — fetch it fleet-wide here; in-flight requests are shared,
Expand All @@ -29,8 +30,8 @@
title={widget_mileage_overview()}
loading={fuelLogStore.processing}
valueFormatter={(value, series) => formatMileage(value, series.fuelType)}
xFormatter={(v: Date) =>
v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })}
xFormatter={(value: Date) => formatDate(value)}
xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })}
>
{#snippet filter()}
<VehicleFilterDropdown
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import StackedAreaChart from '$dashboard/StackedAreaChart.svelte';
import type { DashboardSummary } from '$lib/domain/dashboard';
import { getLocale } from '$lib/paraglide/runtime.js';

let { summary, loading }: { summary: DashboardSummary | null; loading: boolean } = $props();
</script>
Expand All @@ -10,4 +11,5 @@
data={summary?.expenses.monthlyTrend ?? []}
title="Monthly Expense Trend"
{loading}
xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })}
/>
4 changes: 3 additions & 1 deletion src/lib/components/feature/overview/AreaChart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
label,
title,
xFormatter,
xAxisFormatter,
valueFormatter,
loading = false,
bare = false,
Expand All @@ -30,6 +31,7 @@
label: string;
title: string;
xFormatter: (_: Date) => string;
xAxisFormatter: (_: Date) => string;
valueFormatter?: (_: number) => string;
loading?: boolean;
/** Render content only — surrounding card chrome is provided by the parent. */
Expand All @@ -46,7 +48,7 @@
motion: 'tween'
},
xAxis: {
format: (v: Date) => v.toLocaleDateString('en-IN', { month: 'short' })
format: (value: Date) => xAxisFormatter(value)
},
yAxis: {
format: (v: number) => valueFormatter?.(v) ?? v.toString()
Expand Down
4 changes: 3 additions & 1 deletion src/lib/components/feature/overview/VehicleTrendChart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
series,
title,
xFormatter,
xAxisFormatter,
valueFormatter,
loading = false,
bare = false,
Expand All @@ -25,6 +26,7 @@
series: VehicleTrendSeries[];
title: string;
xFormatter: (_: Date) => string;
xAxisFormatter: (_: Date) => string;
valueFormatter?: (_: number, _series: VehicleTrendSeries) => string;
loading?: boolean;
/** Render content only — surrounding card chrome is provided by the parent. */
Expand Down Expand Up @@ -67,7 +69,7 @@

const chartProps = {
xAxis: {
format: (v: Date) => v.toLocaleDateString('en-IN', { month: 'short' })
format: (value: Date) => xAxisFormatter(value)
},
yAxis: {
format: (v: number) =>
Expand Down
23 changes: 20 additions & 3 deletions src/lib/helper/date.helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { DateValue } from '@internationalized/date';
import { format, parse } from 'date-fns';
import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
import { es, fr, de, hi } from 'date-fns/locale';
import { ar, cs, de, enUS, es, fi, fr, hi, hu, it, pt, ro, ru } from 'date-fns/locale';

export interface FormatConfig {
dateFormat: string;
Expand All @@ -17,15 +17,32 @@ export interface FormatConfig {

const getDateFnsLocale = (locale: string) => {
switch (locale) {
case 'ar':
return ar;
case 'cs':
return cs;
case 'de':
return de;
case 'es':
return es;
case 'fi':
return fi;
case 'fr':
return fr;
case 'de':
return de;
case 'hi':
return hi;
case 'hu':
return hu;
case 'it':
return it;
case 'pt-PT':
return pt;
case 'ro':
return ro;
case 'ru':
return ru;
case 'en':
return enUS;
default:
return undefined;
}
Expand Down
16 changes: 11 additions & 5 deletions src/routes/(app)/fuel/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import { fuelLogStore } from '$stores/fuel-log.svelte';
import { chartStore } from '$stores/chart.svelte';
import { ACCENT } from '$lib/helper/accent-color.helper';
import { formatCurrency, formatMileage } from '$lib/helper/format.helper';
import { formatCurrency, formatDate, formatMileage } from '$lib/helper/format.helper';
import VehicleTrendChart from '$feature/overview/VehicleTrendChart.svelte';
import { getLocale } from '$lib/paraglide/runtime.js';
import FuelLogTab from '$feature/fuel/FuelLogTab.svelte';
import { Features } from '$lib/helper/feature.helper';
import FeaturePageShell from '$feature/shared/FeaturePageShell.svelte';
Expand Down Expand Up @@ -77,16 +78,21 @@
title={widget_fuel_consumption_trend()}
loading={fuelLogStore.processing}
valueFormatter={(value) => `${value.toFixed(1)} L`}
xFormatter={(v: Date) =>
v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })}
xFormatter={(value: Date) => formatDate(value)}
xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })}
/>
<VehicleTrendChart
series={chartStore.mileageByVehicle}
title={widget_mileage_overview()}
loading={fuelLogStore.processing}
valueFormatter={(value, series) => formatMileage(value, series.fuelType)}
xFormatter={(v: Date) =>
v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })}
xFormatter={(value: Date) =>
value.toLocaleDateString(getLocale(), {
day: '2-digit',
month: 'short',
year: 'numeric'
})}
xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })}
/>
</div>

Expand Down
Loading