// // ChargeSessionCompletionSheetView.swift // USB Meter // import SwiftUI struct ChargeSessionCompletionSheetView: View { private enum FinalCheckpoint: String, CaseIterable, Identifiable { case full case skip case custom var id: String { rawValue } var label: String { switch self { case .full: return "Full" case .skip: return "Skip" case .custom: return "Other %" } } } @EnvironmentObject private var appData: AppData @Environment(\.dismiss) private var dismiss let sessionID: UUID let title: String let confirmTitle: String let explanation: String @State private var batteryPercent = "" @State private var finalCheckpoint: FinalCheckpoint = .skip var body: some View { NavigationView { Form { Section( header: ContextInfoHeader( title: "Final Checkpoint", message: explanation ) ) { Picker("Final Battery", selection: $finalCheckpoint) { ForEach(FinalCheckpoint.allCases) { mode in Text(mode.label).tag(mode) } } .pickerStyle(.segmented) if finalCheckpoint == .custom { TextField("Battery %", text: $batteryPercent) .keyboardType(.decimalPad) } } Section { if let refusalReason { Label(refusalReason, systemImage: "exclamationmark.triangle.fill") .font(.footnote) .foregroundColor(.red) Button(role: .destructive) { _ = appData.deleteChargeSession(sessionID: sessionID) dismiss() } label: { Label("Discard Session", systemImage: "trash") } } else if let customCheckpointWarning { Label(customCheckpointWarning, systemImage: "exclamationmark.triangle.fill") .font(.footnote) .foregroundColor(.orange) } else if let sessionWarning { Text(sessionWarning) .font(.footnote) .foregroundColor(.orange) } else if resolvedFinalBatteryPercent == 100 { Text("A final checkpoint at 100% lets the app learn the stop current for this exact charging type when the session data is reliable.") .font(.footnote) .foregroundColor(.secondary) } } } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(confirmTitle) { guard canSave else { return } if appData.stopChargeSession(sessionID: sessionID, finalBatteryPercent: resolvedFinalBatteryPercent) { dismiss() } } .disabled(!canSave) .opacity(canSave ? 1 : 0.45) } } } .navigationViewStyle(StackNavigationViewStyle()) } private var session: ChargeSessionSummary? { appData.chargedDevices .flatMap(\.sessions) .first(where: { $0.id == sessionID }) } private var canSave: Bool { session?.hasSavableChargeData == true } private var refusalReason: String? { canSave ? nil : "This session has no charging data to save. Discard it instead." } private var customCheckpointWarning: String? { guard finalCheckpoint == .custom, batteryPercent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false, parsedBatteryPercent == nil else { return nil } return "Final battery percentage must be between 0 and 100. Save will close the session without a final checkpoint." } private var parsedBatteryPercent: Double? { let normalized = batteryPercent .trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: ",", with: ".") guard let value = Double(normalized), value >= 0, value <= 100 else { return nil } return value } private var resolvedFinalBatteryPercent: Double? { switch finalCheckpoint { case .full: return 100 case .skip: return nil case .custom: return parsedBatteryPercent } } private var sessionWarning: String? { guard let session, session.chargingTransportMode == .wireless, let chargerID = session.chargerID, let charger = appData.chargedDeviceSummary(id: chargerID), charger.chargerIdleCurrentAmps == nil else { return nil } return "This charger has no idle-current measurement, so the final checkpoint will stop the session but will not learn a wireless stop threshold yet." } }