import SwiftUI struct WatchBeforeAfterFlowView: View { enum Stage { case intro, before, beforeFeeling, exercise, after, afterFeeling, result } @EnvironmentObject private var model: WatchAppModel let regulation: WatchRegulationProtocol @State private var stage: Stage = .intro @State private var before: WatchMeasurementSummaryV1? @State private var after: WatchMeasurementSummaryV1? @State private var exerciseStartedAt: Date? @State private var flowError: String? @State private var selectedFeeling = 3 @State private var feelingBefore: Int? @State private var feelingAfter: Int? var body: some View { ScrollView { VStack(spacing: 10) { switch stage { case .intro: Image(systemName: "arrow.left.arrow.right.heart.fill").font(.title).foregroundStyle(WatchTheme.accent) Text("Avant · Exercice · Après").font(.headline).multilineTextAlignment(.center) Text("Deux mesures identiques encadrent l’exercice pour comparer le BPM et la qualité, sans score de stress.") .font(.caption2).foregroundStyle(WatchTheme.secondary).multilineTextAlignment(.center) Button("Mesurer avant") { startMeasurement(stage: .before) }.buttonStyle(.borderedProminent) case .before, .after: measurementContent case .beforeFeeling: feelingContent(title: "Comment vous sentez-vous avant ?") { feelingBefore = selectedFeeling if var measurement = before { measurement.feeling = selectedFeeling measurement.feelingRecordedAt = Date() measurement.updatedAt = Date() before = measurement model.enqueueMeasurement(measurement) } stage = .exercise exerciseStartedAt = Date() Task { do { try await model.haptics.start(protocol: regulation) } catch { flowError = error.localizedDescription } } } case .exercise: if model.haptics.isRunning, let exerciseStartedAt { BreathOrb(regulation: regulation, startedAt: exerciseStartedAt) ProgressView(value: model.haptics.elapsedSeconds, total: regulation.durationSeconds) Button("Arrêter", role: .cancel) { model.haptics.cancel() } } else { Text("Exercice terminé").font(.headline) Button("Mesurer après") { startMeasurement(stage: .after) }.buttonStyle(.borderedProminent) } case .afterFeeling: feelingContent(title: "Comment vous sentez-vous après ?") { feelingAfter = selectedFeeling if var measurement = after { measurement.feeling = selectedFeeling measurement.feelingRecordedAt = Date() measurement.updatedAt = Date() after = measurement model.enqueueMeasurement(measurement) } saveCompletedSession() stage = .result } case .result: resultContent } if let flowError { Text(flowError).font(.caption2).foregroundStyle(.orange) } } .padding(.horizontal, 4) } .navigationTitle("Comparaison") .haptikaScreen() .onChange(of: model.measurement.state) { _, newState in handleMeasurementState(newState) } } @ViewBuilder private var measurementContent: some View { switch model.measurement.state { case let .waitingForFirstSample(seconds): ProgressView() Text("Recherche du signal · \(seconds) s").font(.caption2) case let .collecting(progress): Text(model.measurement.currentBPM.map { "\(Int($0.rounded())) BPM" } ?? "— BPM").font(.title2.bold()) ProgressView(value: progress) Text(stage == .before ? "Mesure avant" : "Mesure après").font(.caption2) case .finishing: ProgressView("Analyse…") default: ProgressView() } Button("Annuler", role: .cancel) { model.measurement.cancel(); stage = .intro } } private var resultContent: some View { VStack(spacing: 9) { Text("Votre comparaison").font(.headline) WatchCard { VStack(alignment: .leading, spacing: 5) { metricRow("Avant", before?.averageBPM) metricRow("Après", after?.averageBPM) if let beforeBPM = before?.averageBPM, let afterBPM = after?.averageBPM { Divider() Text(String(format: "Écart %+.0f BPM", afterBPM - beforeBPM)).foregroundStyle(WatchTheme.accent) } Text("Qualité : \(Int(((before?.quality ?? 0) * 100).rounded())) % → \(Int(((after?.quality ?? 0) * 100).rounded())) %") if let feelingBefore, let feelingAfter { Text("Ressenti : \(feelingBefore)/5 → \(feelingAfter)/5") } } .font(.caption) } Text("Une variation ponctuelle n’est pas un diagnostic.").font(.caption2).foregroundStyle(WatchTheme.secondary) } } private func metricRow(_ label: String, _ bpm: Double?) -> some View { HStack { Text(label) Spacer() Text(bpm.map { "\(Int($0.rounded())) BPM" } ?? "—") } } private func startMeasurement(stage: Stage) { self.stage = stage model.measurement.resetAfterResult() Task { do { try await model.measurement.requestAuthorization() try await model.measurement.start() } catch { flowError = error.localizedDescription } } } private func feelingContent(title: String, onConfirm: @escaping () -> Void) -> some View { VStack(spacing: 10) { Text(title).font(.headline).multilineTextAlignment(.center) Picker("Ressenti", selection: $selectedFeeling) { ForEach(1...5, id: \.self) { score in Text("\(score) / 5").tag(score) } } Button("Continuer", action: onConfirm).buttonStyle(.borderedProminent) } } private func saveCompletedSession() { let endedAt = Date() model.enqueueSession(.init( protocolID: regulation.id, startedAt: exerciseStartedAt ?? endedAt, endedAt: endedAt, durationSeconds: regulation.durationSeconds, state: .completed, beforeMeasurementID: before?.id, afterMeasurementID: after?.id, feelingBefore: feelingBefore, feelingAfter: feelingAfter )) } private func handleMeasurementState(_ state: WatchMeasurementState) { guard case let .completed(summary) = state else { return } if stage == .before { before = summary model.measurement.resetAfterResult() selectedFeeling = 3 stage = .beforeFeeling } else if stage == .after { after = summary model.measurement.resetAfterResult() selectedFeeling = 3 stage = .afterFeeling } } }