Best Value: candidate-priced fills, key copy, detail total
This commit is contained in:
@@ -566,10 +566,34 @@ struct ContentView: View {
|
||||
func source(_ basis: FuelSavingsDistanceBasis) -> String {
|
||||
basis == .roadRoute ? "road route" : "straight-line estimate"
|
||||
}
|
||||
let price = station.prices[selectedFuel].map { String(format: "%.1fp/L", $0) } ?? "—"
|
||||
let baselinePrice = baseline?.prices[selectedFuel].map { String(format: "%.1fp/L", $0) } ?? "—"
|
||||
func amount(_ value: FuelSavingsFillAmount) -> String {
|
||||
switch value.unit {
|
||||
case .pounds: return String(format: "£%.2f", value.value)
|
||||
case .litres: return String(format: "%.2f L", value.value)
|
||||
}
|
||||
}
|
||||
func rawMoney(_ value: Double?) -> String {
|
||||
value.map { String(format: "£%.6f", $0) } ?? "—"
|
||||
}
|
||||
let candidatePriceValue = station.prices[selectedFuel]
|
||||
let baselinePriceValue = baseline?.prices[selectedFuel]
|
||||
let price = candidatePriceValue.map { String(format: "%.1fp/L", $0) } ?? "—"
|
||||
let baselinePrice = baselinePriceValue.map { String(format: "%.1fp/L", $0) } ?? "—"
|
||||
let priceDifference = candidatePriceValue.flatMap { candidate in
|
||||
baselinePriceValue.map { String(format: "%+.1fp/L", $0 - candidate) }
|
||||
} ?? "—"
|
||||
let litresBasis = sessionFuelSavingsAmount.unit == .pounds ? "candidate price" : "fixed litres"
|
||||
let rows: [(String, String)] = [
|
||||
("Station", "\(station.name) (\(station.id))"),
|
||||
("Settings default fill-up", amount(fuelSavingsSettings.preferredAmount)),
|
||||
("Effective/session fill-up", amount(sessionFuelSavingsAmount)),
|
||||
("Fill-up unit", sessionFuelSavingsAmount.unit.displayName),
|
||||
("MPG", String(format: "%.1f UK MPG", fuelSavingsSettings.mpg)),
|
||||
("Candidate price", price),
|
||||
("Baseline price", baselinePrice),
|
||||
("Price difference", priceDifference),
|
||||
("Candidate litres", result.map { String(format: "%.6f L · %@", $0.fillLitres, litresBasis) } ?? "—"),
|
||||
("Gross saving raw", rawMoney(result?.grossSavingGBP)),
|
||||
("Displayed distance", "\(distanceUnit.format(stationDistance.displayDistanceKM)) · \(source(stationDistance.displaySource))"),
|
||||
("Outward road", meters(stationDistance.outwardMeters)),
|
||||
("Return road", stationDistance.hasMeasuredReturn ? meters(stationDistance.returnMeters) : "inferred or unavailable"),
|
||||
@@ -581,12 +605,11 @@ struct ContentView: View {
|
||||
("Station journey", miles(journey?.miles)),
|
||||
("Baseline journey", miles(baselineJourney?.miles)),
|
||||
("Additional journey", miles(result?.extraDistanceMiles)),
|
||||
("Additional fuel used", result.map { String(format: "%.6f L", $0.extraFuelLitres) } ?? "—"),
|
||||
("Confidence", result?.journeyConfidence.rawValue ?? "estimated"),
|
||||
("Fuel price", price),
|
||||
("Baseline price", baselinePrice),
|
||||
("Gross saving", result.map { FuelSavingsFormat.money($0.grossSavingGBP) } ?? "—"),
|
||||
("Travel cost", result.map { FuelSavingsFormat.money($0.extraDrivingCostGBP) } ?? "—"),
|
||||
("Net saving", result.map { FuelSavingsFormat.money($0.netSavingGBP) } ?? "—")
|
||||
("Travel cost raw", rawMoney(result?.extraDrivingCostGBP)),
|
||||
("Net saving raw", rawMoney(result?.netSavingGBP)),
|
||||
("Displayed net saving", result?.headline() ?? "—")
|
||||
]
|
||||
return FuelSavingsJourneyDiagnostics(rows: rows)
|
||||
#else
|
||||
@@ -1104,6 +1127,13 @@ struct ContentView: View {
|
||||
ensureBestValueAlertModeIsAvailable()
|
||||
updateAlertMonitor()
|
||||
}
|
||||
.onChange(of: sessionFuelSavingsAmount) { _, _ in
|
||||
// This remains a session-only override: do not write it to FuelStore.
|
||||
// Any active Best Value alert/Live Activity must nevertheless use
|
||||
// the same effective amount as rows, details, favourites and sorting.
|
||||
updateAlertMonitor()
|
||||
updateLiveActivity()
|
||||
}
|
||||
.onChange(of: scenePhase) { _, newPhase in
|
||||
if newPhase == .active {
|
||||
// Never start location tracking while onboarding is on screen —
|
||||
|
||||
@@ -118,7 +118,11 @@ struct FuelSavingsSummaryView: View {
|
||||
// matched neither the listing nor each other.
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
capsule(for: result, compact: false)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.accessibilityLabel(accessibilityHeadline(for: result))
|
||||
LabeledContent("Your fill-up", value: amountText)
|
||||
LabeledContent("Fuel you'll receive", value: String(format: "%.2f litres", result.fillLitres))
|
||||
.monospacedDigit()
|
||||
if let extraTravel = FuelSavingsFormat.extraTravelLabel(
|
||||
extraDistanceMiles: result.extraDistanceMiles,
|
||||
unit: distanceUnit,
|
||||
@@ -134,9 +138,26 @@ struct FuelSavingsSummaryView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Compared with \(baselineStationName ?? "your nearest station") for \(amountText).").font(.caption).foregroundStyle(.secondary)
|
||||
LabeledContent("Fuel-price saving", value: FuelSavingsFormat.money(result.grossSavingGBP))
|
||||
Text("Compared with \(baselineStationName ?? "your nearest station").")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
LabeledContent(
|
||||
result.grossSavingGBP >= 0 ? "Fuel-price saving" : "Fuel-price difference",
|
||||
value: result.grossSavingGBP >= 0
|
||||
? FuelSavingsFormat.money(result.grossSavingGBP)
|
||||
: "Costs \(FuelSavingsFormat.money(abs(result.grossSavingGBP))) more"
|
||||
)
|
||||
LabeledContent("Extra driving cost", value: FuelSavingsFormat.money(result.extraDrivingCostGBP))
|
||||
Divider()
|
||||
LabeledContent {
|
||||
Text(result.headline())
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(capsuleColor(for: result.status))
|
||||
.monospacedDigit()
|
||||
} label: {
|
||||
Text("Total")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
if result.journeyConfidence.isEstimated {
|
||||
Text("Estimated from straight-line or incomplete route data — refined once both routes are measured.")
|
||||
.font(.caption2)
|
||||
|
||||
@@ -618,7 +618,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
let content = UNMutableNotificationContent()
|
||||
content.sound = .default
|
||||
content.title = "Best Value \(fuel.displayName) nearby: \(brand)"
|
||||
content.body = "\(station.name) could leave you \(result.headline()). Tap for directions."
|
||||
content.body = "\(station.name): \(result.headline()). Tap for directions."
|
||||
addAlertRequest(content: content, station: station)
|
||||
logAlert(.fired, "Best Value alert scheduled — \(brand) · \(result.headline())")
|
||||
lastAlert = "Best Value · \(brand) · \(result.headline())"
|
||||
@@ -923,7 +923,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
content.sound = .default
|
||||
let brand = candidate.station.brand.isEmpty ? candidate.station.name : candidate.station.brand
|
||||
content.title = "Best Value \(fuel.displayName) nearby: \(brand)"
|
||||
content.body = "\(candidate.station.name) could leave you \(candidate.result.headline()). Tap for directions."
|
||||
content.body = "\(candidate.station.name): \(candidate.result.headline()). Tap for directions."
|
||||
let baseResult = "Best Value · \(brand) · \(candidate.result.headline()) · radius \(unit.format(radiusKM))"
|
||||
lastTestResult = baseResult
|
||||
logAlert(.fired, "Best Value test alert scheduled — \(brand) · \(candidate.result.headline())")
|
||||
|
||||
@@ -249,7 +249,7 @@ struct StationsView: View {
|
||||
VStack(spacing: 10) {
|
||||
keyCard(
|
||||
icon: "checkmark.circle.fill", color: .green,
|
||||
title: "Best value", blurb: "Within 1.5p of the cheapest",
|
||||
title: "Good price", blurb: "Within 1.5p of the cheapest",
|
||||
threshold: "≤1.5p")
|
||||
keyCard(
|
||||
icon: "equal.circle.fill", color: .orange,
|
||||
@@ -298,6 +298,17 @@ struct StationsView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
|
||||
}
|
||||
|
||||
Section("How Best Value works") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Best Value compares each station with the closest one. It works out what you could save at the pump, then takes off the fuel used for the extra return journey.")
|
||||
Text("Your fill-up amount and MPG are used to show whether you could save overall.")
|
||||
Text("Results are marked “Approx.” until road distances are available.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Key")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"Show %lld more (%lld remaining)" = "Show %lld more (%lld remaining)";
|
||||
"All %lld stations shown" = "All %lld stations shown";
|
||||
"Key" = "Key";
|
||||
"Best value" = "Best value";
|
||||
"Good price" = "Good price";
|
||||
"Within 1.5p of the cheapest" = "Within 1.5p of the cheapest";
|
||||
"Okay" = "Okay";
|
||||
"Within 4p of the cheapest" = "Within 4p of the cheapest";
|
||||
@@ -35,6 +35,10 @@
|
||||
"Add to favourites" = "Add to favourites";
|
||||
"Remove from favourites" = "Remove from favourites";
|
||||
"Colours match each station's rating on the list" = "Colours match each station's rating on the list";
|
||||
"How Best Value works" = "How Best Value works";
|
||||
"Best Value compares each station with the closest one. It works out what you could save at the pump, then takes off the fuel used for the extra return journey." = "Best Value compares each station with the closest one. It works out what you could save at the pump, then takes off the fuel used for the extra return journey.";
|
||||
"Your fill-up amount and MPG are used to show whether you could save overall." = "Your fill-up amount and MPG are used to show whether you could save overall.";
|
||||
"Results are marked “Approx.” until road distances are available." = "Results are marked “Approx.” until road distances are available.";
|
||||
|
||||
/* Favourites tab */
|
||||
"No favourites yet" = "No favourites yet";
|
||||
@@ -194,6 +198,7 @@
|
||||
"Cheapest station" = "Cheapest station";
|
||||
"Alert type" = "Alert type";
|
||||
"Best Value" = "Best Value";
|
||||
"Total" = "Total";
|
||||
"Minimum saving" = "Minimum saving";
|
||||
"Set up Best Value first so FuelBoard can account for your fill size, MPG and travel cost." = "Set up Best Value first so FuelBoard can account for your fill size, MPG and travel cost.";
|
||||
"Set up Best Value first so Best Value alerts can account for your fill size, MPG and travel cost." = "Set up Best Value first so Best Value alerts can account for your fill size, MPG and travel cost.";
|
||||
|
||||
Reference in New Issue
Block a user