diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/changelogs/1.3.0.md b/changelogs/1.3.0.md new file mode 100644 index 00000000..1bdb9aa6 --- /dev/null +++ b/changelogs/1.3.0.md @@ -0,0 +1,115 @@ +# Release Notes – v1.3.0 (2026-03-19) + +## Overview + +v1.3.0 focuses on the settings experience, notification delivery controls, clearer overview charts, and a broad cleanup of shared helpers and notification flows. It also improves demo seed data so mileage and cost trends look more realistic over time. + +## Major Features + +### Settings UX Improvements + +- Expanded all settings accordions by default for quicker access +- Made settings forms more responsive with two- and three-column layouts where space allows +- Added a compact switch-based style for feature flags +- Updated notification provider forms to default new providers to enabled +- Simplified channel subscription controls in the provider dialog +- Extracted reusable settings sections, field helpers, and display blocks +- Reused shared tab shell and form composition patterns in settings + +### Notification Delivery Controls + +- Added a toggle to enable or disable scheduled notification delivery +- Kept the delivery schedule tied to the notification settings state +- Disabled schedule inputs automatically when scheduling is turned off +- Added webhook and Gotify providers alongside email +- Improved provider toggling, cron scheduling, and notification send templates +- Allowed editing providers without exposing keys/tokens + +### Overview Charts + +- Added an average reference line to mileage and cost graphs +- Displayed the average as a top-right label with unit-aware formatting +- Formatted tooltip values with the correct units for mileage and currency +- Added a unit-aware average formatter for chart tooltips and labels +- Improved the chart presentation with clearer dashed average lines + +### Demo Seed Data + +- Updated seeded mileage values to progress more naturally over time +- Added small mileage deviations and realistic fuel cost variation +- Made the overall seed data better reflect real-world usage patterns +- Refined seeded notifications and maintenance history for more natural trends + +## Configuration Changes + +- Added `notificationProcessingEnabled` to control scheduled notification delivery +- Kept `notificationProcessingSchedule` as the cron expression for delivery timing +- Added default config values for LPG and CNG fuel units +- Preserved mileage unit format settings for distance-per-fuel and fuel-per-distance +- Expanded settings schema and defaults for feature flags and notification delivery +- Updated chart formatting helpers to support unit-aware mileage and currency display +- Added shared config handling and merge helpers for notification provider settings +- Seeded the new config entries automatically for demo setups + +## Environment/Runtime Changes + +- Demo seeding now generates more realistic mileage and cost trends +- Notification scheduler respects the new enabled/disabled config state +- Overview charts now format tooltip and average values using app units +- The app now keeps chart and settings formatting aligned with the active locale/config + +## UI/UX Improvements + +- Compact provider channel subscriptions in the add/edit provider dialog +- Better chart labeling and readability in the overview section +- More consistent settings layout across personalization, units, and feature flags +- Better mobile behavior and tighter spacing across settings and dialogs +- Improved loading skeletons and shared record card layouts across the UI + +## Architecture & Shared Helpers + +- Consolidated route error helpers and standardized backend service responses +- Added typed payload helpers for domain and service layers +- Reduced shared store and form `any` usage +- Extracted reusable table, skeleton, and formatter helpers +- Reused shared resource state and feature card layouts across the UI +- Improved notification provider config merge and service date helpers +- Added helper reuse across vehicle, fuel, maintenance, insurance, and reminders + +## Localization & Messaging + +- Continued moving hardcoded UI text into i18n message functions +- Added or refined translated messages for settings, notifications, and charts +- Improved localized labels across dashboard, forms, and notifications + +## Developer Experience + +- Added MCP support for the repo's Svelte workflow +- Upgraded dependencies and fixed follow-up lint/check issues +- Cleaned up formatting, typing, and shared abstractions across the codebase + +## Bug Fixes & Improvements + +- Fixed reactive binding issues in settings forms so inputs update correctly +- Fixed notification delivery scheduling state so it no longer re-enables unexpectedly after save +- Improved unit display for mileage and cost values in chart tooltips and labels +- Fixed settings accordions to stay expanded by default +- Fixed provider add/edit flow to keep new providers enabled by default +- Fixed fuel and maintenance sorting when records share dates +- Fixed NaN-prone calculations and lint issues carried over from refactors + +## Migration Notes + +- No breaking changes were introduced +- Existing settings and data remain compatible + +## Environment Variables + +- No new environment variables were required for this release +- Existing runtime behavior continues to use the current app configuration and demo flags + +## Known Issues + +- None reported at release time + +For detailed commit history, see the [compare view](https://github.com/javedh-dev/tracktor/compare/v1.2.0...v1.3.0). diff --git a/messages/en.json b/messages/en.json index 6c4e825c..c7536046 100644 --- a/messages/en.json +++ b/messages/en.json @@ -45,6 +45,7 @@ "settings_desc_mileage_format": "Choose how fuel efficiency is displayed", "settings_mileage_format_distance_per_fuel": "Distance per Fuel (e.g., km/L, mpg)", "settings_mileage_format_fuel_per_distance": "Fuel per Distance (e.g., L/100km)", + "settings_mileage_format_uk_mpg": "UK MPG (miles per imperial gallon)", "settings_desc_theme": "Choose your preferred theme", "settings_desc_custom_css": "CSS Styles for customizing the interface", "settings_select_language": "Select language", diff --git a/messages/fi.json b/messages/fi.json index 375c6b46..5333a127 100644 --- a/messages/fi.json +++ b/messages/fi.json @@ -3,6 +3,7 @@ "hello_world": "Hei, {name} fi:stä!", "app_name": "Tracktor", "app_title": "Autotallisi", + "app_new_update_available": "Uusi päivitys saatavilla. Ladataan uudelleen..!", "app_add_vehicle": "Lisää ajoneuvo", "app_empty_select_message": "Valitse ajoneuvo nähdäksesi sen yksityiskohdat", "app_empty_select_hint": "Valitse ajoneuvo nähdäksesi sen kojelaudan", @@ -79,7 +80,6 @@ "nav_insurance": "Vakuutus", "nav_pollution": "Päästöt", "nav_reminders": "Muistutukset", - "nav_settings": "Asetukset", "tools_export_data": "Vie data", "tools_import_data": "Tuo data", "vehicle_form_make_label": "Valmistaja", @@ -163,7 +163,7 @@ "fuel_add_title": "Lisää tankkaus", "col_date": "Päivämäärä", "col_odometer": "Matkamittari", - "col_filled": "Täytetty", + "col_filled": "Täysi tankki", "col_missed_last": "Edellinen ohitettu", "col_fuel_amount": "Polttoainemäärä", "col_cost": "Kustannus", @@ -204,6 +204,8 @@ "notifications_section_alerts": "Hälytykset", "notifications_mark_done_title": "Merkitse muistutus valmiiksi", "notifications_mark_done_aria": "Merkitse {type} muistutus valmiiksi", + "notifications_mark_all_read_title": "Merkitse kaikki luetuiksi", + "notifications_mark_all_read_aria": "Merkitse kaikki ilmoitukset luetuiksi", "notifications_overdue_days": "{days} päivää myöhässä", "notifications_due_today": "Määräaika tänään", "notifications_due_tomorrow": "Määräaika huomenna", @@ -449,5 +451,104 @@ "pollution_recurrence_type_fixed": "Kiinteä päättymispäivä", "pollution_recurrence_type_yearly": "Uusiutuu vuosittain", "pollution_recurrence_type_monthly": "Uusiutuu kuukausittain", - "pollution_recurrence_type_no_end": "Ei päättymispäivämäärää" + "pollution_recurrence_type_no_end": "Ei päättymispäivämäärää", + "file_drop_existing_note": "Liitetiedosto (Napauta nähdäksesi)", + "fuel_import_step_1_title": "Vaihe 1 : Lataa CSV tiedosto", + "fuel_import_step_1_desc": "Valitse erotinmerkkiä käyttävä tekstitiedosto, joka sisältää polttoaineen kulutustietosi, aloittaaksesi tuontiprosessin", + "fuel_import_drop_placeholder": "Liitä erotinmerkillinen teksti tai napauta selataksesi", + "fuel_import_headers_checkbox": "Ensimmäinen rivi sisältää otsikot", + "fuel_import_delimiter_title": "Erotin", + "fuel_import_delimiter_desc": "Valitse merkki, joka erottaa kentät", + "fuel_import_date_format_title": "Päivämäärän muoto", + "fuel_import_date_format_desc": "Määritä CSV-tiedostosi päivämäärille käytettävä muoto.", + "fuel_import_error_no_headers": "Otsikoita ei havaittu. Päivitä csv.helper.ts palauttaaksesi otsikot.", + "fuel_import_step_2_title": "Vaihe 2 : CSV-sarakkeiden kartoitus", + "fuel_import_step_2_desc": "Määritä CSV-tiedostosi sarakkeet vastaaviin polttoainelokin kenttiin. Pakolliset kentät on merkitty merkinnällä *.", + "fuel_import_step_3_title": "Vaihe 3 : Esikatsele ja tuo", + "fuel_import_step_3_desc": "Tarkista tuotavat tiedot esikatselusta.", + "fuel_import_no_preview": "Ei esikatseltavaa tietoa vielä. Toteuta jäsentely csv.helper.ts-tiedostossa rivien täyttämiseksi.", + "fuel_import_success": "Tuotu onnistuneesti {count} polttoainelokia.", + "fuel_import_failed_count": "Tuotu {imported}, epäonnistui {failed}", + "fuel_import_error_generic": "Polttoainelokin tuonti epäonnistui.", + "fuel_import_vehicle_label": "Ajoneuvo:", + "fuel_import_delimiter_comma": "Pilkku ( , )", + "fuel_import_delimiter_semicolon": "Puolipiste ( ; )", + "fuel_import_delimiter_tab": "Sarkain ( \\t )", + "fuel_import_delimiter_pipe": "Pystyviiva ( | )", + "fuel_import_delimiter_custom": "Muokattu", + "fuel_import_date_error": "Jotkin rivit sisältävät virheellisesti muotoiltuja päivämääriä \"{format}\"", + "fuel_import_date_format_placeholder": "esim., MM/DD/YYYY", + "fuel_import_date_invalid": "Virheellinen päivämäärä", + "fuel_import_no_vehicle": "Ajoneuvoa ei valittu", + "fuel_import_col_date_hint": "Tankkauspäivämäärä", + "fuel_import_col_odometer_hint": "Mittarilukema tankkaushetkellä", + "fuel_import_col_fuel_hint": "Tankattu määrä tai energia", + "fuel_import_col_cost_hint": "Kokonaiskustannus", + "fuel_import_col_filled_hint": "Onko tämä täysi tankki/lataus?", + "fuel_import_col_missed_hint": "Jäikö edellinen merkintä väliin?", + "fuel_import_col_notes_hint": "Lisätietoja", + "autocomplete_placeholder": "Kirjoita tai valitse...", + "autocomplete_loading": "Ladataan ehdotuksia...", + "autocomplete_no_results": "Ehdotuksia ei löydy. Voit kirjoittaa uuden arvon.", + "input_date_placeholder": "Valitse päivämäärä", + "loading_default_message": "Ladataan...", + "dropzone_placeholder_image": "Napauta tai vedä kuva ladataksesi", + "dropzone_placeholder_attachment": "Pudota kuva tänne tai napauta valitaksesi", + "dropzone_placeholder_default": "Napauta tai vedä tiedostoja ladataksesi", + "dropzone_error_single_file": "Lataa vain yksi tiedosto.", + "dropzone_error_file_size": "Tiedoston koko ylittää sallitun enimmäiskoon {size}.", + "dropzone_unknown_file": "Tuntematon tiedosto", + "dropzone_uploading": "Ladataan...", + "dropzone_supports": "Tuettu: {types}", + "dropzone_max_size": "Enimmäiskoko: {size}", + "dropzone_error_file_type": "Tiedoston tyyppi ei ole tuettu", + "dropzone_hint_accept_limit": "{types} enintään {size}", + "vehicle_details_color_aria": "Väri", + "vehicle_details_close_aria": "Sulje", + "attachment_link_view_title": "Näytä liite", + "file_preview_not_available": "Esikatselu ei ole saatavilla", + "file_preview_download_hint": "Tätä tiedostotyyppiä ei voi esikatsella suoraan. Lataa tiedosto tarkastellaksesi sitä.", + "file_preview_download_button": "Lataa tiedosto", + "file_preview_aria_download": "Lataa", + "file_preview_aria_close": "Sulje", + "theme_toggle_label": "Vaihda teemaa", + "fuel_log_edit": "Muokkaa", + "fuel_log_delete": "Poista", + "fuel_log_menu_open": "Avaa valikko", + "fuel_log_delete_success": "Poistetut polttoainelokimerkinnät", + "fuel_log_menu_sheet_title": "Päivitä polttoainelokimerkintä", + "fuel_log_delete_error": "Polttoainelokimerkinnän poistamisessa tapahtui virhe", + "color_picker_label": "Valitse väri", + "reminder_type_maintenance": "Huolto", + "reminder_type_insurance": "Vakuutuksen uusiminen", + "reminder_type_pollution": "Päästömittaus / katsastus", + "reminder_type_registration": "Rekisteröinti / ajoneuvovero", + "reminder_type_inspection": "Katsastus", + "reminder_type_custom": "Custom", + "alert_type_insurance": "Vakuutus", + "alert_type_pucc": "Päästötodistus", + "alert_status_expired_ago": "{label} vanheni {days} päivää sitten", + "alert_status_expires_in": "{label} vanhenee {days} päivässä", + "alert_status_valid_for": "{label} voimassa {days} päivää", + "alert_insurance_active_no_end": "Vakuutus on voimassa toistaiseksi", + "alert_pucc_active_no_end": "Päästötodistus on voimassa toistaiseksi", + "alert_record_not_found": "{label}-tietoja ei löydy. Lisää tiedot pysyäksesi vaatimusten mukaisena.", + "settings_error_format_not_valid": "Virheellinen muoto", + "common_kilogram_unit": "Kilogrammaa (kg)", + "common_pound_unit": "Paunaa (lb)", + "settings_section_fuel_types": "Polttoaineen tyyppi", + "settings_section_fuel_types_desc": "Valitse mittayksikkö kullekin polttoaineelle.", + "fuel_type_petrol_diesel": "Bensiini/Diesel", + "theme_slate": "Liuskekivi", + "theme_stone": "Kivi", + "theme_red": "Punainen", + "theme_rose": "Ruusu", + "theme_blue": "Sininen", + "theme_green": "Vihreä", + "theme_purple": "Violetti", + "theme_orange": "Oranssi", + "theme_yellow": "Keltainen", + "theme_teal": "Sinivihreä", + "theme_indigo": "Indigo", + "theme_pink": "Vaaleanpunainen" } diff --git a/messages/hu.json b/messages/hu.json index 35bfd08e..ed2722cc 100644 --- a/messages/hu.json +++ b/messages/hu.json @@ -205,6 +205,8 @@ "notifications_section_alerts": "Riasztások", "notifications_mark_done_title": "Emlékeztető megjelölése készként", "notifications_mark_done_aria": "{type} emlékeztető megjelölése készként", + "notifications_mark_all_read_title": "Az összes megjelölése olvasottként", + "notifications_mark_all_read_aria": "Összes értesítés megjelölése olvasottként", "notifications_overdue_days": "{days} napja lejárt", "notifications_due_today": "Ma esedékes", "notifications_due_tomorrow": "Holnap esedékes", diff --git a/src/lib/components/feature/settings/SettingsModal.svelte b/src/lib/components/feature/settings/SettingsModal.svelte index 9cbd4ec1..7ca53d7f 100644 --- a/src/lib/components/feature/settings/SettingsModal.svelte +++ b/src/lib/components/feature/settings/SettingsModal.svelte @@ -50,7 +50,7 @@ unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), mileageUnitFormat: z - .enum(['distance-per-fuel', 'fuel-per-distance']) + .enum(['distance-per-fuel', 'fuel-per-distance', 'uk-mpg']) .default('distance-per-fuel'), theme: z.string().default('light'), customCss: z.string().optional(), @@ -177,6 +177,10 @@ { value: 'fuel-per-distance', label: m.settings_mileage_format_fuel_per_distance() + }, + { + value: 'uk-mpg', + label: m.settings_mileage_format_uk_mpg() } ]; diff --git a/src/lib/components/feature/settings/SettingsPersonalizationTab.svelte b/src/lib/components/feature/settings/SettingsPersonalizationTab.svelte index da4127e9..e7fe2ed5 100644 --- a/src/lib/components/feature/settings/SettingsPersonalizationTab.svelte +++ b/src/lib/components/feature/settings/SettingsPersonalizationTab.svelte @@ -105,7 +105,7 @@ /> {m.common_example_prefix()} - {isValidFormat(formData.dateFormat).ex || m.common_invalid_format()} + {isValidFormat($formData.dateFormat).ex || m.common_invalid_format()} {/snippet} diff --git a/src/lib/components/feature/vehicle/VehicleCard.svelte b/src/lib/components/feature/vehicle/VehicleCard.svelte index b638b72c..842cac7b 100644 --- a/src/lib/components/feature/vehicle/VehicleCard.svelte +++ b/src/lib/components/feature/vehicle/VehicleCard.svelte @@ -9,7 +9,6 @@ import BellRing from '@lucide/svelte/icons/bell-ring'; import Info from '@lucide/svelte/icons/info'; import { vehicleStore } from '$stores/vehicle.svelte'; - import { browser } from '$app/environment'; import IconButton from '$appui/IconButton.svelte'; import DeleteConfirmation from '$appui/DeleteConfirmation.svelte'; import * as Card from '$ui/card'; @@ -36,7 +35,7 @@ const performDelete = async (vehicleId: string) => { deleteVehicle(vehicleId).then((res) => { if (res.status == 'OK') { - fetchVehicles(); + vehicleStore.refreshVehicles(); toast.success(m.vehicle_delete_success()); } else { toast.error(res.error || m.vehicle_delete_error()); @@ -44,13 +43,6 @@ }); }; - const fetchVehicles = () => { - if (browser) { - const pin = localStorage.getItem('userPin') || undefined; - if (pin) vehicleStore.refreshVehicles(); - } - }; - // Dynamic image URL - fallback to default if vehicle doesn't have image const imageUrl = $derived( vehicle.image ? withBase(`/api/files/${vehicle.image}`) : '/default-vehicle.png' diff --git a/src/lib/components/ui/calendar/calendar.svelte b/src/lib/components/ui/calendar/calendar.svelte index 519ac642..dd04ece0 100644 --- a/src/lib/components/ui/calendar/calendar.svelte +++ b/src/lib/components/ui/calendar/calendar.svelte @@ -5,6 +5,7 @@ import type { ButtonVariant } from '../button/button.svelte'; import { isEqualMonth, type DateValue } from '@internationalized/date'; import type { Snippet } from 'svelte'; + import { getLocale } from '$lib/paraglide/runtime.js'; let { ref = $bindable(null), @@ -14,7 +15,7 @@ weekdayFormat = 'short', buttonVariant = 'ghost', captionLayout = 'label', - locale = 'en-US', + locale = getLocale(), months: monthsProp, years, monthFormat: monthFormatProp, diff --git a/src/lib/domain/maintenance.ts b/src/lib/domain/maintenance.ts index 662106e1..9f0b28d1 100644 --- a/src/lib/domain/maintenance.ts +++ b/src/lib/domain/maintenance.ts @@ -28,7 +28,7 @@ export const maintenanceSchema = z.object({ .string() .min(2, 'It must be more than 1 character.') .max(50, 'It must be less than 50 characters.'), - cost: z.float32().positive(), + cost: z.float32().nonnegative(), notes: z.string().nullable(), attachment: z.string().nullable() }); diff --git a/src/lib/helper/format.helper.ts b/src/lib/helper/format.helper.ts index 9128565d..33467f9f 100644 --- a/src/lib/helper/format.helper.ts +++ b/src/lib/helper/format.helper.ts @@ -210,6 +210,15 @@ const getMileageUnit = (vehicleType: string): string => { return `${fuelLabel}/100${distanceUnit}`; } + // only show uk mpg if miles and liters are used + if ( + configs.mileageUnitFormat === 'uk-mpg' && + configs.unitOfDistance === 'mile' && + fuelUnit === 'liter' + ) { + return 'mpg'; + } + // Default: distance-per-fuel (e.g., km/L, mpg) const mileageUnit = `${configs.unitOfDistance}-per-${fuelUnit}`; const label = safeUnitLabel(mileageUnit); @@ -230,6 +239,15 @@ const formatMileage = (mileage: number, vehicleType: string): string => { return `${mileage.toFixed(2)} ${fuelLabel}/100${distanceUnit}`; } + // only show uk mpg if miles and liters are used + if ( + configs.mileageUnitFormat === 'uk-mpg' && + configs.unitOfDistance === 'mile' && + fuelUnit === 'liter' + ) { + return `${mileage.toFixed(2)} mpg`; + } + // Default: distance-per-fuel (e.g., km/L, mpg) const mileageUnit = `${configs.unitOfDistance}-per-${fuelUnit}`; return ( diff --git a/src/lib/helper/settings-form.helper.ts b/src/lib/helper/settings-form.helper.ts index 01aa9f3d..a2e4c4f8 100644 --- a/src/lib/helper/settings-form.helper.ts +++ b/src/lib/helper/settings-form.helper.ts @@ -25,28 +25,33 @@ export function createSettingsConfigSchema( isValidTimezone: (value: string) => boolean, options: SettingsSchemaOptions = {} ) { - const baseSchema = z.object({ - dateFormat: z.string().refine((fmt) => isValidFormat(fmt).valid, 'Format not valid'), - locale: z.string().min(2), - timezone: z.string().min(3).refine(isValidTimezone, 'Invalid timzone value.'), - currency: z.string().min(1, 'Currency is required'), - unitOfDistance: z.enum(['kilometer', 'mile']), - unitOfVolume: z.enum(['liter', 'gallon']), - unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), - unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), - mileageUnitFormat: z - .enum(['distance-per-fuel', 'fuel-per-distance']) - .default('distance-per-fuel'), - theme: z.string().default('light'), - customCss: z.string().optional(), - featureFuelLog: z.boolean().default(true), - featureMaintenance: z.boolean().default(true), - featurePucc: z.boolean().default(true), - featureReminders: z.boolean().default(true), - featureInsurance: z.boolean().default(true), - featureOverview: z.boolean().default(true), - notificationProcessingEnabled: z.boolean().default(true) - }); + const baseSchema = z + .object({ + dateFormat: z.string().refine((fmt) => isValidFormat(fmt).valid, 'Format not valid'), + locale: z.string().min(2), + timezone: z.string().min(3).refine(isValidTimezone, 'Invalid timzone value.'), + currency: z.string().min(1, 'Currency is required'), + unitOfDistance: z.enum(['kilometer', 'mile']), + unitOfVolume: z.enum(['liter', 'gallon']), + unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), + unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), + mileageUnitFormat: z + .enum(['distance-per-fuel', 'fuel-per-distance', 'uk-mpg']) + .default('distance-per-fuel'), + theme: z.string().default('light'), + customCss: z.string().optional(), + featureFuelLog: z.boolean().default(true), + featureMaintenance: z.boolean().default(true), + featurePucc: z.boolean().default(true), + featureReminders: z.boolean().default(true), + featureInsurance: z.boolean().default(true), + featureOverview: z.boolean().default(true), + notificationProcessingEnabled: z.boolean().default(true) + }) + .refine((obj) => { + if (obj.mileageUnitFormat !== 'uk-mpg') return true; + return obj.unitOfDistance === 'mile' && obj.unitOfVolume === 'liter'; + }, 'UK MPG calculation requires unit of distance to be miles and unit of volume to be litres.'); if (!options.includeNotificationProcessingSchedule) { return baseSchema; @@ -108,6 +113,10 @@ export function createSettingsOptions( { value: 'fuel-per-distance', label: m.settings_mileage_format_fuel_per_distance() + }, + { + value: 'uk-mpg', + label: m.settings_mileage_format_uk_mpg() } ], localeOptions: locales.map((code) => ({ diff --git a/src/lib/types/settings.ts b/src/lib/types/settings.ts index c853ea08..862db3b7 100644 --- a/src/lib/types/settings.ts +++ b/src/lib/types/settings.ts @@ -7,7 +7,7 @@ export interface SettingsFormShape extends Record { unitOfVolume: 'liter' | 'gallon'; unitOfLpg: 'liter' | 'gallon' | 'kilogram' | 'pound'; unitOfCng: 'liter' | 'gallon' | 'kilogram' | 'pound'; - mileageUnitFormat: 'distance-per-fuel' | 'fuel-per-distance'; + mileageUnitFormat: 'distance-per-fuel' | 'fuel-per-distance' | 'uk-mpg'; theme: string; customCss?: string; featureFuelLog: boolean; diff --git a/src/server/services/fuelLogService.ts b/src/server/services/fuelLogService.ts index eeb27a9d..f1a6fbdf 100644 --- a/src/server/services/fuelLogService.ts +++ b/src/server/services/fuelLogService.ts @@ -29,7 +29,8 @@ export const addFuelLog = async ( .insert(schema.fuelLogTable) .values({ ...fuelLogData, - vehicleId: vehicleId + vehicleId: vehicleId, + id: undefined }) .returning(); return createSuccessResponse(fuelLog[0], 'Fuel log added successfully.'); @@ -40,6 +41,16 @@ export const getFuelLogs = async (vehicleId: string): Promise => { const mileageFormatConfig = await db.query.configTable.findFirst({ where: (config, { eq }) => eq(config.key, 'mileageUnitFormat') }); + const distanceUnit = ( + await db.query.configTable.findFirst({ + where: (config, { eq }) => eq(config.key, 'unitOfDistance') + }) + )?.value; + const volumeUnit = ( + await db.query.configTable.findFirst({ + where: (config, { eq }) => eq(config.key, 'unitOfVolume') + }) + )?.value; const mileageFormat = mileageFormatConfig?.value || 'distance-per-fuel'; const fuelLogs = await db.query.fuelLogTable.findMany({ @@ -102,6 +113,9 @@ export const getFuelLogs = async (vehicleId: string): Promise => { if (mileageFormat === 'fuel-per-distance') { // Fuel per 100 distance units (e.g., L/100km, gal/100mi) mileage = (totalFuel / distance) * 100; + } else if (mileageFormat === 'uk-mpg' && distanceUnit === 'mile' && volumeUnit === 'liter') { + // Miles per imperial gallon (mpg) + mileage = (distance / totalFuel) * 4.546; } else { // Distance per fuel unit (e.g., km/L, mpg) - default mileage = distance / totalFuel; diff --git a/src/server/services/insuranceService.ts b/src/server/services/insuranceService.ts index 15e2f461..d62383f0 100644 --- a/src/server/services/insuranceService.ts +++ b/src/server/services/insuranceService.ts @@ -29,7 +29,8 @@ export const addInsurance = async ( .insert(schema.insuranceTable) .values({ ...sanitizedInsuranceData, - vehicleId: vehicleId + vehicleId: vehicleId, + id: undefined }) .returning(); return createSuccessResponse(insurance[0], 'Insurance details added successfully.'); diff --git a/src/server/services/notificationService.ts b/src/server/services/notificationService.ts index cff14869..ebc5fcaa 100644 --- a/src/server/services/notificationService.ts +++ b/src/server/services/notificationService.ts @@ -17,6 +17,7 @@ import { type GeneratedNotification } from './notification-service.helper'; import { createFailureResponse, createSuccessResponse } from './service-response.helper'; +import { getAppConfigByKey } from './configService'; type NotificationType = keyof typeof NOTIFICATION_TYPES; type NotificationSource = keyof typeof NOTIFICATION_SOURCES; @@ -114,10 +115,18 @@ async function buildPuccNotifications(vehicleId: string): Promise { + let puccEnabled = true; + try { + const puccConfig = await getAppConfigByKey('featurePucc'); + if (puccConfig.success) puccEnabled = puccConfig.data?.value !== 'false'; + } catch { + // key not in DB yet — default to enabled + } + const [reminders, insurances, puccCertificates] = await Promise.all([ buildReminderNotifications(vehicleId), buildInsuranceNotifications(vehicleId), - buildPuccNotifications(vehicleId) + puccEnabled ? buildPuccNotifications(vehicleId) : Promise.resolve([]) ]); return sortNotificationsByDueDate([...reminders, ...insurances, ...puccCertificates]); diff --git a/src/server/services/pollutionCertificateService.ts b/src/server/services/pollutionCertificateService.ts index 47f7a323..2be03dc9 100644 --- a/src/server/services/pollutionCertificateService.ts +++ b/src/server/services/pollutionCertificateService.ts @@ -28,7 +28,8 @@ export const addPollutionCertificate = async ( .insert(schema.pollutionCertificateTable) .values({ ...sanitizedPayload, - vehicleId: vehicleId + vehicleId: vehicleId, + id: undefined }) .returning(); return createSuccessResponse( diff --git a/src/server/services/vehicleService.ts b/src/server/services/vehicleService.ts index 0a99fefd..9c431571 100644 --- a/src/server/services/vehicleService.ts +++ b/src/server/services/vehicleService.ts @@ -10,8 +10,9 @@ type VehiclePayload = Omit; type VehicleMutationPayload = Omit; function serializeVehiclePayload(vehicleData: VehicleMutationPayload) { + const { id: _, ...data } = vehicleData as VehicleMutationPayload & { id?: unknown }; return { - ...vehicleData, + ...data, customFields: vehicleData.customFields ? JSON.stringify(vehicleData.customFields) : null }; }