feat give each prescribed exercise its own routine sub-row

Render the routine table with one sub-row per prescribed exercise. The workout name spans its exercises through a row-group heading, and each sub-row carries its own exercise-name and rep-band cells. Drop the stimuli-count column: a count is neither a plan detail a trainee acts on nor, per HD1, an achievement to display. Reps stay a band the load calibrates against, never a target.

Commit
28caa6358d50c9fb9cd40d61f21790790912377d
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/web/pages.ml
index cad4a3fa..cc88adad 100644..100644
@@ -342,17 +342,22 @@
342 342 let id_string (id : Repository.routine_id) = (id :> string)
343 343 let error_id input_id = input_id ^ "-error"
344 344
345 Removed: let describe_prescription prescription =
345 Added: (* The exercise name for one prescribed stimulus, without the rep window. A
346 Added: pre-exhaust names both movements. *)
347 Added: let prescription_exercise prescription =
348 Added: match Prescription.Stimulus.delivery prescription with
349 Added: | Prescription.Stimulus.Single exercise -> Exercise.name exercise
350 Added: | Prescription.Stimulus.Pre_exhaust { isolation; compound } ->
351 Added: Printf.sprintf "%s into %s, no pause" (Exercise.name isolation)
352 Added: (Exercise.name compound)
353 Added:
354 Added: (* The prescribed rep window, phrased as a band the load calibrates against.
355 Added: Reps are an outcome, never a target (HD1, "Proper Weight Selection"). *)
356 Added: let prescription_reps prescription =
346 357 let min_reps, max_reps =
347 358 Prescription.Rep_range.bounds (Prescription.Stimulus.rep_range prescription)
348 359 in
349 Removed: let window = Printf.sprintf "%d-%d reps" min_reps max_reps in
350 Removed: match Prescription.Stimulus.delivery prescription with
351 Removed: | Prescription.Stimulus.Single exercise ->
352 Removed: Printf.sprintf "%s — %s" (Exercise.name exercise) window
353 Removed: | Prescription.Stimulus.Pre_exhaust { isolation; compound } ->
354 Removed: Printf.sprintf "%s into %s, no pause — %s" (Exercise.name isolation)
355 Removed: (Exercise.name compound) window
360 Added: Printf.sprintf "%d-%d reps" min_reps max_reps
356 361
357 362 let extension_select ?(selected = "") ?(invalid = false) ~input_id () =
358 363 let error_attrs =
@@ -563,32 +568,43 @@
563 568 [ txt "Workout" ];
564 569 tag "th"
565 570 [ Dream_html.string_attr "scope" "col" ]
566 Removed: [ txt "Stimuli" ];
571 Added: [ txt "Exercise" ];
567 572 tag "th"
568 573 [ Dream_html.string_attr "scope" "col" ]
569 Removed: [ txt "Prescription" ];
574 Added: [ txt "Reps" ];
570 575 ];
571 576 ];
577 Added: (* One sub-row per prescribed exercise. The workout name spans its
578 Added: exercises through a row-group heading, so each exercise gets its
579 Added: own name and rep-band cells and no stimuli count is shown. *)
572 580 tag "tbody" []
573 Removed: (List.map
581 Added: (List.concat_map
574 582 (fun workout ->
575 583 let stimuli = Prescription.Workout.stimuli workout in
576 Removed: tag "tr" []
577 Removed: [
578 Removed: tag "th"
579 Removed: [ Dream_html.string_attr "scope" "row" ]
580 Removed: [ txt "%s" (Prescription.Workout.name workout) ];
581 Removed: tag "td" [] [ txt "%d" (List.length stimuli) ];
582 Removed: tag "td" []
583 Removed: [
584 Removed: tag "ul" []
585 Removed: (List.map
586 Removed: (fun stimulus ->
587 Removed: tag "li" []
588 Removed: [ txt "%s" (describe_prescription stimulus) ])
589 Removed: stimuli);
590 Removed: ];
591 Removed: ])
584 Added: let name = Prescription.Workout.name workout in
585 Added: List.mapi
586 Added: (fun index stimulus ->
587 Added: let workout_cell =
588 Added: if index = 0 then
589 Added: [
590 Added: tag "th"
591 Added: [
592 Added: Dream_html.string_attr "scope" "rowgroup";
593 Added: Dream_html.string_attr "rowspan" "%d"
594 Added: (List.length stimuli);
595 Added: ]
596 Added: [ txt "%s" name ];
597 Added: ]
598 Added: else []
599 Added: in
600 Added: tag "tr" []
601 Added: (workout_cell
602 Added: @ [
603 Added: tag "td" []
604 Added: [ txt "%s" (prescription_exercise stimulus) ];
605 Added: tag "td" [] [ txt "%s" (prescription_reps stimulus) ];
606 Added: ]))
607 Added: stimuli)
592 608 (Prescription.Routine.workouts routine));
593 609 ];
594 610 ]
test/test_web.ml
index d6c12f5c..d812c3ab 100644..100644
@@ -603,6 +603,18 @@
603 603 "includes prescription" true
604 604 (contains ~substring:"Dumbbell Flyes into Incline Presses" page);
605 605 Alcotest.(check bool)
606 Added: "heads an exercise column" true
607 Added: (contains ~substring:"Exercise" page);
608 Added: Alcotest.(check bool)
609 Added: "heads a reps column" true
610 Added: (contains ~substring:"Reps" page);
611 Added: Alcotest.(check bool)
612 Added: "gives each exercise its own sub-row" true
613 Added: (contains ~substring:"rowgroup" page);
614 Added: Alcotest.(check bool)
615 Added: "drops the stimuli count column" false
616 Added: (contains ~substring:"Stimuli" page);
617 Added: Alcotest.(check bool)
606 618 "shows primary navigation" true
607 619 (contains ~substring:"primary-nav" page);
608 620 Alcotest.(check bool)