Best Value: explain price age per surface instead of one blanket warning

- Station detail now separates the station's own report age from the
  selected fuel's update time, so a recent station-wide report no longer
  reads as an old price.
- Warning copy names the fuel and says which side is weak: the station's
  price is not individually dated, or the baseline's is. The misleading
  "unverified or older than 7 days" line is gone.
- Wording stays "Closest" throughout; new copy uses Unleaded / Premium /
  Diesel rather than the internal grade codes.
- Localisation: new and updated keys for the added copy.
This commit is contained in:
FuelBoard Contributor
2026-10-15 21:37:35 +01:00
parent 7c2eeb2604
commit 9e77ee2947
8 changed files with 222 additions and 78 deletions
+47 -23
View File
@@ -401,28 +401,27 @@ struct ContentView: View {
/// (computing a journey for each) and `savingsLookup` immediately scanned them
/// all AGAIN to fill the per-station map. Both now share one result.
///
/// The baseline is chosen with EXACTLY the loop and guards of
/// `FuelSavingsEstimator.baselineStation`, so the row labelled "Closest
/// station" cannot move — and it is still selected when the feature is
/// unconfigured (it names that row), while the maps are only built when the
/// labels will actually be shown.
/// The baseline is chosen with the same journey and selected-fuel freshness
/// guards as `FuelSavingsEstimator.bestValueBaselineStation` when configured;
/// with the feature off, the ordinary Closest station still names that row.
private func savingsScaffold() -> (baseline: FuelStation?, journeys: [String: FuelSavingsJourney]) {
guard let location else { return (nil, [:]) }
let configured = fuelSavingsSettings.isConfigured
guard fuelSavingsSettings.isConfigured else {
let baseline = FuelSavingsEstimator.baselineStation(stations, fuel: selectedFuel) {
savingsJourneyMiles(for: $0, location: location)
}
return (baseline, [:])
}
var journeys: [String: FuelSavingsJourney] = [:]
var bestStation: FuelStation?
var bestMiles = Double.infinity
for station in stations where station.prices[selectedFuel] != nil {
guard let journey = savingsJourneyMiles(for: station, location: location) else { continue }
if configured { journeys[station.id] = journey }
let miles = journey.miles
guard miles.isFinite, miles >= 0 else { continue }
if miles < bestMiles {
bestMiles = miles
bestStation = station
if let journey = savingsJourneyMiles(for: station, location: location) {
journeys[station.id] = journey
}
}
return (bestStation, configured ? journeys : [:])
let baseline = FuelSavingsEstimator.bestValueBaselineStation(stations, fuel: selectedFuel) {
journeys[$0.id]
}
return (baseline, journeys)
}
/// Row/detail savings lookup with the comparison baseline and the per-station
@@ -455,15 +454,15 @@ struct ContentView: View {
}
}
/// The nearest station that sells the selected fuel — the Best Value
/// comparison baseline. It is chosen with the SAME journey values used by
/// the savings calculation, so a cached road route can change the baseline
/// and keep the "extra return travel" labels synchronized with the
/// cost calculation. When no road route is cached, the journey falls back to
/// straight-line out-and-back, and the result is marked as an estimate.
/// The Closest station with an eligible selected-fuel price — the Best Value
/// comparison baseline. Ordinary Closest sorting still includes stale stations.
/// It uses the same journey values as the savings calculation, so a cached
/// road route can change the baseline and keep the "extra return travel"
/// labels synchronized with the cost calculation. Without a cached route,
/// the journey falls back to straight-line and the result is an estimate.
private var nearestSavingsBaseline: FuelStation? {
guard let location else { return nil }
return FuelSavingsEstimator.baselineStation(
return FuelSavingsEstimator.bestValueBaselineStation(
stations,
fuel: selectedFuel,
journeyMiles: { [self] station in savingsJourneyMiles(for: station, location: location) }
@@ -803,6 +802,10 @@ struct ContentView: View {
fuelSavings: savingsLookup(baseline: scaffold.baseline,
journeys: scaffold.journeys)(station),
fuelSavingsAmount: sessionFuelSavingsAmount,
savingsPriceConfidence: FuelPriceFreshness.ComparisonConfidence.assess(
station: station, fuel: selectedFuel, baseline: scaffold.baseline),
savingsPriceWarning: FuelPriceFreshness.comparisonWarning(
station: station, fuel: selectedFuel, baseline: scaffold.baseline),
savingsBaselineStationName: scaffold.baseline?.name,
journeyDiagnostics: journeyDiagnostics(for: station, baseline: scaffold.baseline),
isFavourite: favouriteIDs.contains(station.id),
@@ -1119,6 +1122,11 @@ struct ContentView: View {
}
}
.onChange(of: fuelSavingsSettings) { old, new in
// An explicit Settings default edit supersedes the temporary slider
// override. Changes to MPG/enabled alone leave the session fill intact.
if old.preferredAmount != new.preferredAmount {
sessionFuelSavingsAmount = new.preferredAmount
}
// Fuel Savings changed in Settings while the app was running: the
// effective sort follows automatically (no stored sort is touched).
#if DEBUG
@@ -1524,6 +1532,12 @@ struct ContentView: View {
fuelSavingsAmount: $sessionFuelSavingsAmount,
savingsForStation: savingsLookup(baseline: baselineStation, journeys: scaffold.journeys),
journeyDiagnosticsForStation: { station in journeyDiagnostics(for: station, baseline: baselineStation) },
savingsPriceConfidenceForStation: { station in
FuelPriceFreshness.ComparisonConfidence.assess(station: station, fuel: selectedFuel, baseline: baselineStation)
},
savingsPriceWarningForStation: { station in
FuelPriceFreshness.comparisonWarning(station: station, fuel: selectedFuel, baseline: baselineStation)
},
onToggleFavourite: toggleFavourite,
onRefresh: { await refresh(force: true) }
)
@@ -1545,6 +1559,12 @@ struct ContentView: View {
fuelSavingsAmount: $sessionFuelSavingsAmount,
savingsForStation: savingsLookup(baseline: baselineStation, journeys: scaffold.journeys),
journeyDiagnosticsForStation: { station in journeyDiagnostics(for: station, baseline: baselineStation) },
savingsPriceConfidenceForStation: { station in
FuelPriceFreshness.ComparisonConfidence.assess(station: station, fuel: selectedFuel, baseline: baselineStation)
},
savingsPriceWarningForStation: { station in
FuelPriceFreshness.comparisonWarning(station: station, fuel: selectedFuel, baseline: baselineStation)
},
onToggleFavourite: toggleFavourite,
onReorder: reorderFavourites,
onHistoryUnavailable: { if dataStatus == .live { dataStatus = .connectionProblem } },
@@ -1782,6 +1802,8 @@ struct StationRow: View {
/// compact banner does not print it today, but a row that carried a hard
/// zero would print "for £0" the moment it did — pass the real figure.
var fuelSavingsAmount: FuelSavingsFillAmount = FuelSavingsSettings.defaultPreferredAmount
var savingsPriceConfidence: FuelPriceFreshness.ComparisonConfidence = .ineligible
var savingsPriceWarning: String? = nil
var onToggleFavourite: () -> Void = {}
var onShowDetails: (FuelStation) -> Void = { _ in }
@@ -1942,6 +1964,8 @@ struct StationRow: View {
FuelSavingsSummaryView(
availability: fuelSavings,
amount: fuelSavingsAmount,
priceConfidence: savingsPriceConfidence,
priceWarning: savingsPriceWarning,
compact: true,
distanceUnit: distanceUnit
)