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:
+47
-23
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user