Add EV charging discovery feature (disabled by flag)
- Create EVChargingService using MapKit POI search for EV chargers - Add ItineraryBuilder.enrichWithEVChargers() for post-planning enrichment - Update TravelSection in TripDetailView with expandable charger list - Add FeatureFlags.enableEVCharging toggle (default: false) - Include EVChargingFeature.md documenting API overhead 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -313,11 +313,19 @@ final class TripCreationViewModel {
|
||||
let result = planningEngine.planItineraries(request: request)
|
||||
|
||||
switch result {
|
||||
case .success(let options):
|
||||
case .success(var options):
|
||||
guard !options.isEmpty else {
|
||||
viewState = .error("No valid itinerary found")
|
||||
return
|
||||
}
|
||||
|
||||
// Enrich with EV chargers if requested and feature is enabled
|
||||
if FeatureFlags.enableEVCharging && needsEVCharging {
|
||||
print("[TripCreation] Enriching \(options.count) options with EV chargers...")
|
||||
options = await ItineraryBuilder.enrichWithEVChargers(options)
|
||||
print("[TripCreation] EV charger enrichment complete")
|
||||
}
|
||||
|
||||
// Store preferences for later conversion
|
||||
currentPreferences = preferences
|
||||
|
||||
|
||||
@@ -535,12 +535,14 @@ struct TripCreationView: View {
|
||||
Divider()
|
||||
.overlay(Theme.surfaceGlow(colorScheme))
|
||||
|
||||
// EV Charging
|
||||
ThemedToggle(
|
||||
label: "EV Charging Needed",
|
||||
isOn: $viewModel.needsEVCharging,
|
||||
icon: "bolt.car"
|
||||
)
|
||||
// EV Charging (feature flagged)
|
||||
if FeatureFlags.enableEVCharging {
|
||||
ThemedToggle(
|
||||
label: "EV Charging Needed",
|
||||
isOn: $viewModel.needsEVCharging,
|
||||
icon: "bolt.car"
|
||||
)
|
||||
}
|
||||
|
||||
// Drivers
|
||||
ThemedStepper(
|
||||
|
||||
@@ -623,6 +623,11 @@ struct GameRow: View {
|
||||
struct TravelSection: View {
|
||||
let segment: TravelSegment
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@State private var showEVChargers = false
|
||||
|
||||
private var hasEVChargers: Bool {
|
||||
!segment.evChargingStops.isEmpty
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -632,39 +637,85 @@ struct TravelSection: View {
|
||||
.frame(width: 2, height: 16)
|
||||
|
||||
// Travel card
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
// Icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Theme.cardBackgroundElevated(colorScheme))
|
||||
.frame(width: 44, height: 44)
|
||||
VStack(spacing: 0) {
|
||||
// Main travel info
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
// Icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Theme.cardBackgroundElevated(colorScheme))
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: "car.fill")
|
||||
.foregroundStyle(Theme.routeGold)
|
||||
Image(systemName: "car.fill")
|
||||
.foregroundStyle(Theme.routeGold)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Travel")
|
||||
.font(.system(size: Theme.FontSize.micro, weight: .semibold))
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
|
||||
Text("\(segment.fromLocation.name) → \(segment.toLocation.name)")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .medium))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(segment.formattedDistance)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Text(segment.formattedDuration)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Travel")
|
||||
.font(.system(size: Theme.FontSize.micro, weight: .semibold))
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
// EV Chargers section (if available)
|
||||
if hasEVChargers {
|
||||
Divider()
|
||||
.background(Theme.routeGold.opacity(0.2))
|
||||
|
||||
Text("\(segment.fromLocation.name) → \(segment.toLocation.name)")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .medium))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
Button {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
||||
showEVChargers.toggle()
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
Image(systemName: "bolt.fill")
|
||||
.foregroundStyle(.green)
|
||||
.font(.system(size: 12))
|
||||
|
||||
Spacer()
|
||||
Text("\(segment.evChargingStops.count) EV Charger\(segment.evChargingStops.count > 1 ? "s" : "") Along Route")
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(segment.formattedDistance)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Text(segment.formattedDuration)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
Spacer()
|
||||
|
||||
Image(systemName: showEVChargers ? "chevron.up" : "chevron.down")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
.padding(.vertical, Theme.Spacing.sm)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if showEVChargers {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(segment.evChargingStops) { charger in
|
||||
EVChargerRow(charger: charger)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
.padding(.bottom, Theme.Spacing.sm)
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackground(colorScheme).opacity(0.7))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay {
|
||||
@@ -680,6 +731,82 @@ struct TravelSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - EV Charger Row
|
||||
|
||||
struct EVChargerRow: View {
|
||||
let charger: EVChargingStop
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
// Connector line indicator
|
||||
VStack(spacing: 0) {
|
||||
Rectangle()
|
||||
.fill(.green.opacity(0.3))
|
||||
.frame(width: 1, height: 8)
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 6, height: 6)
|
||||
Rectangle()
|
||||
.fill(.green.opacity(0.3))
|
||||
.frame(width: 1, height: 8)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Text(charger.name)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.lineLimit(1)
|
||||
|
||||
chargerTypeBadge
|
||||
}
|
||||
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
if let address = charger.location.address {
|
||||
Text(address)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
|
||||
Text("•")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
|
||||
Text("~\(charger.formattedChargeTime) charge")
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var chargerTypeBadge: some View {
|
||||
let (text, color) = chargerTypeInfo
|
||||
Text(text)
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.foregroundStyle(color)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(color.opacity(0.15))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
private var chargerTypeInfo: (String, Color) {
|
||||
switch charger.chargerType {
|
||||
case .supercharger:
|
||||
return ("Supercharger", .red)
|
||||
case .dcFast:
|
||||
return ("DC Fast", .blue)
|
||||
case .level2:
|
||||
return ("Level 2", .green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Share Sheet
|
||||
|
||||
struct ShareSheet: UIViewControllerRepresentable {
|
||||
|
||||
Reference in New Issue
Block a user