View raw

1 (** View models: the simple data structures the presenter renders. 2 3 The Clean Architecture rule for crossing a boundary is that the data passed 4 is a plain structure, never an entity or a database row. These DTOs are that 5 structure. A handler (the controller) maps a repository record or a core 6 entity into a view model, and the presenter ({!Pages}) renders the view 7 model without ever naming an inner-circle type. 8 9 Every page now crosses the boundary as a DTO. No page names a core entity. 10 Add a page DTO here before the presenter renders it. *) 11 12 module Viewer = struct 13 type t = { username : string } 14 (** The signed-in trainee, as the shell needs them: a name to show, nothing 15 more. Replaces passing the [Trainee.t] entity into the presenter. *) 16 end 17 18 module App_feedback = struct 19 type t = { 20 id : string; (** Opaque feedback identity, already stringified. *) 21 author : string; 22 contributions : int; 23 submitted_at : string; (** Pre-formatted "YYYY-MM-DD HH:MM" in UTC. *) 24 message : string; 25 upvotes : int; 26 viewer_upvoted : bool; 27 viewer_owns : bool; 28 } 29 (** One application-feedback entry, ready to render. Carries no timestamp 30 type, no opaque id, no repository record — only what the view shows. *) 31 end 32 33 module Feedback = struct 34 type report = { 35 at_unix : int; (** Report time in Unix seconds, for chronological order. *) 36 signals : string list; (** Each signal already described, for the list. *) 37 } 38 (** One subjective-feedback report, ready to render. Carries no [Feedback] 39 variant and no timestamp type. *) 40 end 41 42 module Routine = struct 43 type exercise = { name : string } 44 (** One prescribed exercise line, its name already phrased. *) 45 46 type position = 47 | Elsewhere 48 | Next of { recovery : string option } 49 (** The workout the cycle reaches next. [recovery] is the phrased 50 recovery status while the trainee still recovers. *) 51 52 type workout = { 53 name : string; 54 exercises : exercise list; 55 rest_after : string; 56 (** The phrased rest that follows the workout. The last workout's rest 57 names the workout that starts the cycle again. *) 58 position : position; 59 } 60 61 type t = { name : string; workouts : workout list } 62 (** A routine as the timeline renders it: workouts in cycle order, each with 63 the rest that gates the next one. No rep bands, no [Prescription] entity. 64 *) 65 end 66 67 module Routine_choice = struct 68 type t = { id : string; name : string; workout_count : int } 69 (** One selectable routine on the chooser: its id for the select form, its 70 name, and how many workouts the cycle holds. *) 71 end 72 73 module Upcoming_workout = struct 74 type exercise = { 75 name : string; 76 latest_load : float option; 77 latest_reps : int option; 78 } 79 80 type group = { exercises : exercise list } 81 82 type t = { 83 routine_name : string; 84 name : string; 85 scheduled_on : string; 86 groups : group list; 87 } 88 (** A prescribed workout that has no logging record yet. Latest performance is 89 evidence for the next attempt, not a repetition target. *) 90 end 91 92 module Home = struct 93 type gate = 94 | Ready 95 | Recovering of { status : string } 96 (** Whether the next workout may begin, and the recovery status line to 97 show while it may not. *) 98 99 type t = { 100 routine_id : string; (** For the begin/override forms. *) 101 routine_name : string; 102 next_workout : Upcoming_workout.t; 103 gate : gate; 104 } 105 (** The overview while no workout is in progress. The recovery policy has 106 already decided [gate] in the use case. The page only renders it. *) 107 end 108 109 module Workout = struct 110 type movement = { 111 name : string; (** The exercise name shown for this input row. *) 112 load_field : string; (** Form field name for the load input. *) 113 reps_field : string; (** Form field name for the reps input. *) 114 reps_label : string; 115 load_value : string option; 116 (** Pre-fill when correcting a recorded slot. *) 117 reps_value : string option; 118 previous_load : string option; 119 (** The most recent prior load for this exercise, pre-formatted as the 120 numeric record to beat (for example ["12"]). [None] when the 121 exercise has no earlier observation. It is an observed fact, not a 122 target — the set still ends at failure. *) 123 previous_reps : string option; 124 (** The most recent prior reps for this exercise, pre-formatted as the 125 numeric record to beat (for example ["22"]). [None] exactly when 126 [previous_load] is [None] — the pair comes from the same 127 observation. *) 128 } 129 (** One input row of a slot's record/correction form. The controller emits one 130 movement for a single-exercise slot and two for a pre-exhaust pair, so the 131 presenter renders whatever movements it is given and never inspects the 132 prescription's [delivery] shape itself. *) 133 134 type slot = { 135 index : int; 136 label : string; (** Exercise label for the accordion summary. *) 137 movements : movement list; 138 recorded_summary : string option; 139 (** [Some description] once the slot is filled. [None] while 140 outstanding. Drives the recorded/outstanding branch without an 141 entity. *) 142 selected_extension : string; 143 (** The ending code to pre-select when correcting a recorded slot ("", 144 "forced", "negatives", "static"). *) 145 selected_extension_amount : string; 146 (** Reps for forced reps/negatives or seconds for a static hold. 147 Defaults to one for an outstanding slot. *) 148 } 149 150 type t = { 151 name : string; (** The workout (prescription) name, for the page title. *) 152 date : string; 153 (** Workout start date, pre-formatted as "day month year" in UTC. *) 154 slots : slot list; 155 active_slot : int; 156 started_at_unix : int; (** Workout start, for the elapsed timer. *) 157 overridden : bool; (** Begun before recovery finished. *) 158 all_recorded : bool; (** Every prescribed slot has a record. *) 159 } 160 (** A workout view: the slot the trainee is on, the record/correction state of 161 each slot, and the timer and override facts. Carries no Evidence or 162 Prescription entity. Extension/ending selection stays in the presenter 163 form, since its option set is fixed UI vocabulary, not domain data. *) 164 end 165 166 module Exercise_comparison = struct 167 type t = { 168 exercise_name : string; 169 current_load : float; 170 current_reps : int; 171 previous_load : float option; 172 previous_reps : int option; 173 } 174 (** One observed exercise effort and its optional previous performance. *) 175 end 176 177 module Stimulus_group = struct 178 type t = { rows : Exercise_comparison.t list } 179 (** The exercises delivered by one stimulus, in performance order. *) 180 end 181 182 module Recorded_workout = struct 183 type t = { 184 id : string; (** Record id for edit and delete links. *) 185 imported : bool; 186 (** True for a promoted third-party import. Shown read-only; its title 187 is the source title verbatim, and an oxblood outline pill, not text 188 on the title, signals its imported status. It feeds progress per 189 exercise but is never a prescribed native record. *) 190 at_unix : int; 191 (** Start time in Unix seconds, for ordering and selection. *) 192 workout_name : string; 193 performed_on : string; 194 duration_previous_seconds : int option; 195 (** The most recent earlier workout with the same prescription name, 196 strictly before this one, in seconds, when both finished. [None] for 197 a first performance of this workout or an unfinished one. *) 198 duration_seconds : int option; 199 (** This workout's own elapsed seconds, when finished. Carried with the 200 previous duration so the presenter can derive a signed delta. *) 201 performed_ago : string; 202 (** Time since the workout started, already phrased for the view. *) 203 next_scheduled : string option; 204 (** Time until the next planned occurrence of this workout, already 205 phrased for the view. *) 206 groups : Stimulus_group.t list; 207 (** Exercise rows grouped by the stimulus that delivered them. *) 208 } 209 (** One recorded workout for the selected-day detail panel. It carries the 210 exercise rows grouped by stimulus, or one read-only group for an import. 211 *) 212 end 213 214 module Selected_date = struct 215 type t = { 216 completed : Recorded_workout.t list; 217 upcoming : Upcoming_workout.t option; 218 } 219 (** The selected calendar day: recorded workouts and the planned workout, if 220 the routine schedules one. *) 221 end 222 223 module Calendar = struct 224 type day = { 225 day_of_month : int; 226 workout : bool; 227 scheduled_workout : string option; 228 today : bool; 229 selected : bool; 230 } 231 (** One dated cell. [workout] marks a completed workout. [scheduled_workout] 232 names the next workout planned for that day, when one exists. *) 233 234 type cell = 235 | Blank 236 | Day of day 237 (** [Blank] pads the leading and trailing days of the grid that fall 238 outside the month. *) 239 240 type t = { 241 selected_year : int; 242 selected_month : int; (** 1..12; the month shown. *) 243 weekday_headings : string list; (** Seven short labels, Monday first. *) 244 weeks : cell list list; (** Rows of seven cells, Monday first. *) 245 } 246 (** A single month laid out as a week grid, above the logbook. Prev/next 247 arrows and a Today link navigate months. The handler does the date 248 arithmetic; the presenter renders the grid. *) 249 end 250 251 module Progress_page = struct 252 type point = { 253 date : string; (** Pre-formatted "YYYY-MM-DD" in UTC. *) 254 at_unix : int; (** For x-axis positioning. *) 255 weight : float; 256 reps : int; 257 } 258 (** One observation on a chart: its load and reps at a point in time. *) 259 260 type chart = { 261 exercise_name : string; 262 points : point list; (** Oldest first, at least two. *) 263 } 264 (** One exercise's merged native and imported history, ready to plot as a 265 dual-axis line chart: weight on the left axis, reps on the right. *) 266 267 type t = { charts : chart list } 268 (** The progress page: one chart per exercise that has at least two 269 observations. Exercises with too little history to draw a line are 270 omitted. *) 271 end 272 273 module Import_warning = struct 274 type t = { 275 row : int; (** The source row that the adapter dropped. *) 276 detail : string; (** Why the adapter dropped the row, already phrased. *) 277 } 278 (** One dropped import row, ready to render. The controller phrases the 279 reason, so no adapter warning variant crosses the boundary. *) 280 end 281 282 module Import_set = struct 283 type t = { 284 row : int; (** The source row, for provenance. *) 285 exercise_name : string; (** The export's exercise name, before mapping. *) 286 set_type : string; (** The set type, already phrased. *) 287 weight : string; (** The load, already formatted. *) 288 reps : string; (** The rep count, already formatted. *) 289 mapping : string option; 290 (** The catalog exercise name this set maps to, when confirmed. *) 291 } 292 (** One provisional import set, ready to render. Carries only strings, no 293 source-set entity. *) 294 end 295 296 module Import_workout = struct 297 type t = { 298 id : string; (** The workout id, for the per-workout forms. *) 299 title : string; 300 sets : Import_set.t list; 301 blockers : string list; 302 (** Why the workout cannot promote yet, each already phrased. Empty when 303 the workout can promote. *) 304 acknowledgement : string option; 305 (** The retained normal-set acknowledgement, when present. *) 306 revision : int; (** The workout revision, for provenance display. *) 307 } 308 (** One provisional import workout, ready to render: its sets, its blockers, 309 and its acknowledgement state. No Workout_import entity. *) 310 end 311 312 module Import_review = struct 313 type t = { 314 batch_id : string; 315 warnings : Import_warning.t list; 316 workouts : Import_workout.t list; 317 } 318 (** A provisional import batch under review: the rows dropped on parse and the 319 workouts awaiting cleaning and promotion. No batch entity crosses the 320 boundary. *) 321 end 322