feat return to routine after cancellation

Confirmed cancellation now saves the workout as a partial logbook entry and returns to the Routine tab instead of Home. With no workout in progress, the middle navigation action reverts from 'Current workout' to 'Routine'. The partial entry stays editable through the logbook. Refines the earlier save-and-exit target from Home to the routine.

Commit
c22fd7b60e17f4133cea02ec415ce9c0df510237
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
ENHANCEMENTS.org
index 45203a46..faf32f4b 100644..100644
@@ -62,5 +62,9 @@
62 62 Allow users to submit subjective feedback at any time from the Logbook. Suggest this feedback flow at the end of each workout, while keeping it available independently.
63 63 ** TODO Style the cancellation modal to match the UI
64 64 Style the modal cancellation menu so it matches the overall UI aesthetic.
65 Removed: ** TODO Preserve and navigate after modal workout cancellation
65 Added: ** DONE Preserve and navigate after modal workout cancellation
66 66 When cancellation is confirmed, return the Current Workout tab to the Routine tab, save the workout as a partial logbook entry, and keep it editable later.
67 Added: ** TODO Style form dropdowns to match the UI
68 Added: Style form dropdown menus so they match the overall UI aesthetic.
69 Added: ** TODO Standardize Logbook navigation and add a mobile profile button
70 Added: Always label the logbook tab `Logbook`. Add a dedicated profile button for the user name in the mobile top navigation bar.
lib/web/handlers.ml
index 9d7efbba..0b3ba033 100644..100644
@@ -369,16 +369,17 @@
369 369 | None -> not_found Present.no_workout)
370 370
371 371 (* Leave the current workout unconditionally. Cancelling never shows an
372 Removed: invalid-form response: the workout in progress is saved as-is — an
373 Removed: incomplete record the logbook keeps for later editing — and the user is
374 Removed: sent Home. A forged or missing CSRF token skips the save but still
375 Removed: redirects Home, so the view is always left cleanly. *)
372 Added: invalid-form response: the workout in progress is saved as-is — a partial
373 Added: record the logbook keeps and stays editable later — and the user returns
374 Added: to the Routine tab, where the middle navigation action reverts from
375 Added: "Current workout" to "Routine". A forged or missing CSRF token skips the
376 Added: save but still navigates away, so the view is always left cleanly. *)
376 377 let cancel t trainee request =
377 378 guard_csrf request >>= function
378 Removed: | Error _ -> redirect_to request Routes.home
379 Added: | Error _ -> redirect_to request Routes.routine
379 380 | Ok () ->
380 381 Service.finish t.service trainee.Trainee.id ~ended_at:(t.now ())
381 Removed: >>= fun _ -> redirect_to request Routes.home
382 Added: >>= fun _ -> redirect_to request Routes.routine
382 383
383 384 let routes t =
384 385 [
lib/web/pages.ml
index 8e7372ad..792f838f 100644..100644
@@ -849,8 +849,8 @@
849 849 tag "p" []
850 850 [
851 851 txt
852 Removed: "The workout is saved as-is for later editing, then you \
853 Removed: return Home.";
852 Added: "The workout is saved as a partial logbook entry you can \
853 Added: edit later, then you return to the routine.";
854 854 ];
855 855 tag "div"
856 856 [ class_ "button-group" ]
test/test_web.ml
index 56885e51..a37b758c 100644..100644
@@ -810,14 +810,14 @@
810 810 post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
811 811 in
812 812 let response = post c "/workout/cancel" [ (* no dream.csrf *) ] in
813 Removed: (* No invalid-form response: cancelling always redirects home. A
814 Removed: forged or missing token skips the save, so the workout is left
815 Removed: untouched and still in progress. *)
813 Added: (* No invalid-form response: cancelling always navigates away to the
814 Added: routine. A forged or missing token skips the save, so the workout
815 Added: is left untouched and still in progress. *)
816 816 Alcotest.(check int)
817 Removed: "redirects home without an error page" 303 (status response);
817 Added: "navigates away without an error page" 303 (status response);
818 818 Alcotest.(check bool)
819 Removed: "redirects to the overview" true
820 Removed: (List.mem "/" (Dream.headers response "Location"));
819 Added: "returns to the routine tab" true
820 Added: (List.mem "/routine" (Dream.headers response "Location"));
821 821 Alcotest.(check int)
822 822 "workout still in progress" 200
823 823 (status (get c "/workout")) );
@@ -846,22 +846,23 @@
846 846 ]
847 847 in
848 848 Alcotest.(check int) "recorded a set" 303 (status recorded);
849 Removed: (* Cancel with a valid token. It saves the log as-is and redirects
850 Removed: home. *)
849 Added: (* Cancel with a valid token. It saves the log as a partial entry
850 Added: and returns to the routine tab. *)
851 851 let token = Option.get (csrf_token (body (get c "/workout"))) in
852 852 let cancelled =
853 853 post c "/workout/cancel" [ ("dream.csrf", token) ]
854 854 in
855 Removed: Alcotest.(check int) "cancel redirects home" 303 (status cancelled);
855 Added: Alcotest.(check int)
856 Added: "cancel returns to the routine" 303 (status cancelled);
856 857 Alcotest.(check bool)
857 Removed: "redirects to the overview, not the logbook" true
858 Removed: (List.mem "/" (Dream.headers cancelled "Location"));
858 Added: "returns to the routine tab, not the logbook" true
859 Added: (List.mem "/routine" (Dream.headers cancelled "Location"));
859 860 (* No current workout: the workout view redirects away. *)
860 861 Alcotest.(check int)
861 862 "no workout in progress" 303
862 863 (status (get c "/workout"));
863 Removed: (* The workout was saved as-is: the logbook holds the record and it
864 Removed: opens for later editing. *)
864 Added: (* The workout was saved as a partial entry: the logbook holds the
865 Added: record and it opens for later editing. *)
865 866 let logbook_page = body (get c "/logbook") in
866 867 Alcotest.(check bool)
867 868 "logbook shows the saved workout" true