From 5b3d4e5f9e06df3423b3de2073921bf002fa3fdc Mon Sep 17 00:00:00 2001 From: kutesir Date: Wed, 8 Jul 2026 13:48:26 +0300 Subject: [PATCH] workout: drop seal icon, confirm every destructive quick action (KC-52 follow-up 2) checkmark.seal.fill (unused elsewhere, reads as a generic 'AI verified badge') replaced with checkmark.circle.fill, the app's actual convention. All four state-changing card actions (Finish Anyway, Check All Sets, Clear All Sets, Didn't Train) now confirm via one PendingAction enum + confirmationDialog before executing, each with a tailored message; destructive ones use the .destructive role. The header menu's Clear All Sets (any completion level) also now confirms instead of executing instantly. Co-Authored-By: Claude Opus 4.8 --- KisaniCal/ISSUES.md | 17 +++++ KisaniCal/Views/WorkoutView.swift | 101 ++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 18 deletions(-) diff --git a/KisaniCal/ISSUES.md b/KisaniCal/ISSUES.md index af010bb..9e5f44b 100644 --- a/KisaniCal/ISSUES.md +++ b/KisaniCal/ISSUES.md @@ -1722,3 +1722,20 @@ progress, and there was no quick way to undo an accidental "Check all sets." the undo is the button itself, always one tap away, no separate menu needed. Files: `WorkoutView.swift`, `Components/SharedComponents.swift`. + +### KC-52 — follow-up 2: drop the "seal" icon, confirm every destructive action +User: "whats with the ai tick also add clear all sets with a prompt to confirm +on all these to avoid accidentals." + +- `checkmark.seal.fill` (a generic "verified badge" glyph, unused anywhere else + in the app) replaced with `checkmark.circle.fill` — the codebase's actual + convention (TodayView, TaskContextMenu, etc.). +- All four state-changing quick actions on the card now confirm first via a + single `PendingAction` enum + one `confirmationDialog` (Finish Anyway, + Check All Sets, Clear All Sets, Didn't Train) — each with its own message; + Clear All / Didn't Train use `.destructive` role. "Finish Workout" at 100% + still skips the dialog (nothing to override). +- The header's "…" menu **Clear All Sets** (works at any completion level, not + just 100%) now also confirms before executing — previously instant/silent. + +Files: `WorkoutView.swift`. diff --git a/KisaniCal/Views/WorkoutView.swift b/KisaniCal/Views/WorkoutView.swift index 406dd8c..9909ec9 100644 --- a/KisaniCal/Views/WorkoutView.swift +++ b/KisaniCal/Views/WorkoutView.swift @@ -67,6 +67,7 @@ struct WorkoutView: View { @State private var showAnalytics = false @State private var isReordering = false @State private var trainAnyway = false // override the rest-day state for today + @State private var showClearAllConfirm = false @ObservedObject private var tm = TutorialManager.shared private let restTick = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -108,7 +109,7 @@ struct WorkoutView: View { IButton(icon: "chart.bar.xaxis") { showStats = true } IButton(icon: "clock.arrow.circlepath") { showHistory = true } Menu { - Button { withAnimation(KisaniSpring.snappy) { vm.setAllSetsDone(false) } } label: { + Button(role: .destructive) { showClearAllConfirm = true } label: { Label("Clear All Sets", systemImage: "circle") } Divider() @@ -347,6 +348,18 @@ struct WorkoutView: View { .presentationDetents([.large]) .presentationDragIndicator(.visible) } + .confirmationDialog( + "Clear all logged sets?", + isPresented: $showClearAllConfirm, + titleVisibility: .visible + ) { + Button("Clear All", role: .destructive) { + withAnimation(KisaniSpring.snappy) { vm.setAllSetsDone(false) } + } + Button("Cancel", role: .cancel) {} + } message: { + Text("This un-checks every set in today's workout. It won't undo an already-finished workout.") + } .sheet(isPresented: $showAddSection) { AddSectionSheet().environmentObject(vm) .presentationDetents([.height(240)]) @@ -593,7 +606,23 @@ struct DotProgressCard: View { let onNotCompleted: () -> Void let onUndo: () -> Void - @State private var showFinishConfirm = false + /// Every quick action that changes today's state is confirmed first — a + /// stray tap here shouldn't silently rewrite the day. + private enum PendingAction: Identifiable { + case finish, checkAll, clearAll, notCompleted + var id: Self { self } + + var confirmLabel: String { + switch self { + case .finish: return "Finish Anyway" + case .checkAll: return "Check All" + case .clearAll: return "Clear All" + case .notCompleted: return "Mark Not Completed" + } + } + var isDestructive: Bool { self == .clearAll || self == .notCompleted } + } + @State private var pendingAction: PendingAction? var body: some View { VStack(spacing: 12) { @@ -625,16 +654,51 @@ struct DotProgressCard: View { .padding(14) .cardStyle(borderColor: isDoneToday ? AppColors.green.opacity(0.3) : nil) .confirmationDialog( - "Finish with \(done) of \(total) sets logged?", - isPresented: $showFinishConfirm, - titleVisibility: .visible - ) { - Button("Finish Anyway") { onFinish() } + confirmTitle, + isPresented: Binding(get: { pendingAction != nil }, set: { if !$0 { pendingAction = nil } }), + titleVisibility: .visible, + presenting: pendingAction + ) { action in + Button(action.confirmLabel, role: action.isDestructive ? .destructive : nil) { + perform(action) + } Button("Cancel", role: .cancel) {} - } message: { - Text(total > 0 - ? "You've logged \(Int(progress * 100))% of sets. This still counts as today's workout." - : "No sets logged yet. This still counts as today's workout.") + } message: { action in + Text(confirmMessage(action)) + } + } + + private var confirmTitle: String { + switch pendingAction { + case .finish: return "Finish with \(done) of \(total) sets logged?" + case .checkAll: return "Check every set as done?" + case .clearAll: return "Clear all logged sets?" + case .notCompleted: return "Mark today as not completed?" + case nil: return "" + } + } + + private func confirmMessage(_ action: PendingAction) -> String { + switch action { + case .finish: + return total > 0 + ? "You've logged \(Int(progress * 100))% of sets. This still counts as today's workout." + : "No sets logged yet. This still counts as today's workout." + case .checkAll: + return "This ticks all \(total) sets as done — useful if you trained but didn't log along the way." + case .clearAll: + return "This un-checks all \(total) sets. It won't undo today's logged workout." + case .notCompleted: + return isDoneToday ? "This replaces today's logged workout with \"not completed.\"" : "You can undo this afterward." + } + } + + private func perform(_ action: PendingAction) { + switch action { + case .finish: onFinish() + case .checkAll: onMarkAllSets() + case .clearAll: onClearAllSets() + case .notCompleted: onNotCompleted() } } @@ -654,7 +718,7 @@ struct DotProgressCard: View { @ViewBuilder private var statusBadge: some View { if isDoneToday { - Image(systemName: "checkmark.seal.fill") + Image(systemName: "checkmark.circle.fill") .font(.system(size: 18)).foregroundColor(AppColors.green) } else if isMissedToday { Image(systemName: "moon.zzz.fill") @@ -668,14 +732,15 @@ struct DotProgressCard: View { /// Primary: finish the workout as-is (honest capture, not gated on 100% sets). /// Secondary: the mechanical "tick every box" action (toggles to a clear once - /// all are checked, so a mis-tap is always one tap to undo), and the miss. + /// all are checked), and the explicit miss. Every action here that changes + /// today's state is confirmed first — see `pendingAction`. private var actionRow: some View { VStack(spacing: 8) { Button { - if done == total && total > 0 { onFinish() } else { showFinishConfirm = true } + if done == total && total > 0 { onFinish() } else { pendingAction = .finish } } label: { Label(done == total && total > 0 ? "Finish Workout" : "Finish Anyway", - systemImage: "checkmark.seal.fill") + systemImage: "checkmark.circle.fill") .font(AppFonts.sans(13, weight: .semibold)) .frame(maxWidth: .infinity) .padding(.vertical, 11) @@ -684,7 +749,7 @@ struct DotProgressCard: View { HStack(spacing: 8) { if done == total && total > 0 { - Button(action: onClearAllSets) { + Button { pendingAction = .clearAll } label: { Label("Clear all sets", systemImage: "circle") .font(AppFonts.sans(11, weight: .medium)) .frame(maxWidth: .infinity) @@ -695,7 +760,7 @@ struct DotProgressCard: View { .clipShape(RoundedRectangle(cornerRadius: 9)) .buttonStyle(PressButtonStyle(scale: 0.97)) } else { - Button(action: onMarkAllSets) { + Button { pendingAction = .checkAll } label: { Label("Check all sets", systemImage: "checkmark.circle") .font(AppFonts.sans(11, weight: .medium)) .frame(maxWidth: .infinity) @@ -707,7 +772,7 @@ struct DotProgressCard: View { .buttonStyle(PressButtonStyle(scale: 0.97)) } - Button(action: onNotCompleted) { + Button { pendingAction = .notCompleted } label: { Label("Didn't train", systemImage: "moon.zzz") .font(AppFonts.sans(11, weight: .medium)) .frame(maxWidth: .infinity)