Record per-set actuals and drive the chart and volume from them
Every completed set now writes a SetEntry (reps/weight or seconds), pre-filled from the plan by transition(to:) so the list checkbox, both run flows, and One More all capture for free; reset clears, skip keeps partials. The rest and finish pages show the just-done set as a pill that opens a stepper sheet for correcting reps and weight (2.5 lb / 1.25 kg steps). The Weight Progression chart plots the top-set actual weight and workout volume sums recorded sets, both falling back to the plan for legacy logs via effectiveSetEntries. Storage side of UX #3 rides along: plan weights are Double now. Schema bumps: SplitDocument 2→3, WorkoutDocument 3→4 (a fractional weight fails an older Int decode, and a rewrite would strip the irreplaceable actuals), SwiftData cache 4→5. A per-log updatedAt is reserved for the future cross-device log merge. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
@@ -34,13 +34,16 @@ struct WorkoutDocumentMapperTests {
|
||||
|
||||
private func log(
|
||||
id: String, name: String, order: Int, status: WorkoutStatus,
|
||||
machineSettings: [MachineSetting]? = nil, startedAt: Date? = nil, completedAt: Date? = nil
|
||||
weight: Double = 135, machineSettings: [MachineSetting]? = nil,
|
||||
startedAt: Date? = nil, completedAt: Date? = nil,
|
||||
setEntries: [SetEntry]? = nil, updatedAt: Date? = nil
|
||||
) -> WorkoutLogDocument {
|
||||
WorkoutLogDocument(
|
||||
id: id, exerciseName: name, order: order, sets: 4, reps: 10, weight: 135,
|
||||
id: id, exerciseName: name, order: order, sets: 4, reps: 10, weight: weight,
|
||||
loadType: LoadType.weight.rawValue, durationSeconds: 0, currentStateIndex: 2,
|
||||
status: status.rawValue, notes: "note-\(name)", date: Self.logDate,
|
||||
machineSettings: machineSettings, startedAt: startedAt, completedAt: completedAt
|
||||
machineSettings: machineSettings, startedAt: startedAt, completedAt: completedAt,
|
||||
setEntries: setEntries, updatedAt: updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,11 +67,18 @@ struct WorkoutDocumentMapperTests {
|
||||
start: Self.start, end: Self.end, status: WorkoutStatus.completed.rawValue,
|
||||
createdAt: Self.created, updatedAt: Self.updated,
|
||||
logs: [
|
||||
// order 0: not a machine exercise (nil), never started.
|
||||
// order 0: not a machine exercise (nil), never started, no entries.
|
||||
log(id: "LOG-A", name: "Bench Press", order: 0, status: .notStarted, machineSettings: nil),
|
||||
// order 1: machine exercise, nothing recorded yet (empty ≠ nil), completed with timestamps.
|
||||
log(id: "LOG-B", name: "Leg Press", order: 1, status: .completed, machineSettings: [],
|
||||
startedAt: Self.startedAt, completedAt: Self.completedAt),
|
||||
// order 1: machine exercise, nothing recorded yet (empty ≠ nil), completed with
|
||||
// timestamps, a fractional weight, recorded per-set actuals, and a per-log updatedAt.
|
||||
log(id: "LOG-B", name: "Leg Press", order: 1, status: .completed,
|
||||
weight: 42.5, machineSettings: [],
|
||||
startedAt: Self.startedAt, completedAt: Self.completedAt,
|
||||
setEntries: [
|
||||
SetEntry(reps: 10, weight: 42.5, completedAt: Self.startedAt),
|
||||
SetEntry(reps: 8, weight: 45, completedAt: Self.completedAt),
|
||||
],
|
||||
updatedAt: Self.updated),
|
||||
// order 2: machine exercise with recorded settings, skipped.
|
||||
log(id: "LOG-C", name: "Chest Press", order: 2, status: .skipped,
|
||||
machineSettings: [MachineSetting(name: "Seat Height", value: "4"),
|
||||
@@ -95,9 +105,17 @@ struct WorkoutDocumentMapperTests {
|
||||
#expect(rebuilt.status == WorkoutStatus.completed.rawValue)
|
||||
#expect(rebuilt.logs.count == 3)
|
||||
#expect(rebuilt.logs[0].machineSettings == nil) // not a machine exercise
|
||||
#expect(rebuilt.logs[0].setEntries == nil) // nothing recorded stays nil
|
||||
#expect(rebuilt.logs[0].updatedAt == nil)
|
||||
#expect(rebuilt.logs[1].machineSettings == []) // machine, nothing recorded
|
||||
#expect(rebuilt.logs[1].startedAt == Self.startedAt)
|
||||
#expect(rebuilt.logs[1].completedAt == Self.completedAt)
|
||||
#expect(rebuilt.logs[1].weight == 42.5) // fractional weight survives
|
||||
#expect(rebuilt.logs[1].setEntries == [
|
||||
SetEntry(reps: 10, weight: 42.5, completedAt: Self.startedAt),
|
||||
SetEntry(reps: 8, weight: 45, completedAt: Self.completedAt),
|
||||
])
|
||||
#expect(rebuilt.logs[1].updatedAt == Self.updated) // per-log merge stamp survives
|
||||
#expect(rebuilt.logs[2].machineSettings?.count == 2)
|
||||
#expect(rebuilt.metrics == fullMetrics())
|
||||
#expect(rebuilt.metrics?.hrZoneSeconds == [10, 20, 30, 40, 50])
|
||||
|
||||
Reference in New Issue
Block a user