diff --git a/src/Plan.roc b/src/Plan.roc index 1f8dabf..a309e8e 100644 --- a/src/Plan.roc +++ b/src/Plan.roc @@ -97,7 +97,8 @@ Plan :: [].{ \\SELECT id AS id, COALESCE(created_at,'') AS created_at, COALESCE(target_date,'') AS target_date, \\ COALESCE(session_type,'') AS session_type, COALESCE(detail,'') AS detail, \\ COALESCE(rationale,'') AS rationale, COALESCE(completed_activity_id,0) AS completed_activity_id, - \\ COALESCE(status,'open') AS status, COALESCE(skipped_reason,'') AS skipped_reason + \\ COALESCE(status,'open') AS status, COALESCE(skipped_reason,'') AS skipped_reason, + \\ COALESCE((SELECT substr(a.start_local,1,10) FROM activities a WHERE a.id = planned_sessions.completed_activity_id), '') AS done_date \\FROM planned_sessions \\WHERE (:all = 1 OR (COALESCE(target_date,'') >= '${Metrics.days_to_date_str(mon)}' AND COALESCE(target_date,'') <= '${Metrics.days_to_date_str(mon + 6)}' AND (COALESCE(status,'open') <> 'skipped' OR NOT EXISTS (SELECT 1 FROM planned_sessions p2 WHERE p2.target_date = planned_sessions.target_date AND (COALESCE(p2.status,'open') <> 'skipped' OR p2.id > planned_sessions.id))))) \\ORDER BY target_date DESC, id DESC LIMIT 100 @@ -113,7 +114,8 @@ Plan :: [].{ completed_activity_id = Sqlite.i64("completed_activity_id")(cols)(stmt)? status = Sqlite.str("status")(cols)(stmt)? skipped_reason = Sqlite.str("skipped_reason")(cols)(stmt)? - Ok({ id, created_at, target_date, session_type, detail, rationale, completed_activity_id, status, skipped_reason }) + done_date = Sqlite.str("done_date")(cols)(stmt)? + Ok({ id, created_at, target_date, session_type, detail, rationale, completed_activity_id, status, skipped_reason, done_date }) }, })? # most recent 100 by date, displayed in calendar order @@ -136,11 +138,24 @@ Plan :: [].{ completed_activity_id: p.completed_activity_id, status: p.status, skipped_reason: p.skipped_reason, + done_date: p.done_date, + # A session completed by an activity from ANOTHER day used to render exactly + # like one completed on time — the plan silently implied the work happened on + # the date it was prescribed for. Show the real day when they differ. + status_shown: + if p.status == "done" and p.done_date != "" and p.done_date != p.target_date { + # Full date, year included: `plan all` spans years, so a bare month-day + # would be ambiguous exactly where the log is longest. The wider cell + # costs a line of wrapping in the detail column; that is the cheaper loss. + "done (${dow(p.done_date)} ${p.done_date})" + } else { + p.status + }, }) Output.out!(enriched, |rows_enriched| Render.render_table( ["day", "date", "type", "status", "detail", "id"], - List.map(rows_enriched, |p| [p.day, p.target_date, p.session_type, p.status, p.detail, (p.id).to_str()]), + List.map(rows_enriched, |p| [p.day, p.target_date, p.session_type, p.status_shown, p.detail, (p.id).to_str()]), )) } plan_add! : Str, Str, Str, Str => Try({}, _) diff --git a/tests/e2e.roc b/tests/e2e.roc index a9535b6..0b6633a 100644 --- a/tests/e2e.roc +++ b/tests/e2e.roc @@ -415,6 +415,29 @@ b_plan! = |ctx| { check!("fully-skipped day shows ONE row", strjq!(ctx, ["plan"], "[.data[] | select(.target_date==\"${ctx.today}\")] | length") == "1")? check!("fully-skipped day shows the FINAL tombstone", strjq!(ctx, ["plan"], ".data[] | select(.target_date==\"${ctx.today}\") | .id") == "6")? check!("plan all keeps every draft", strjq!(ctx, ["plan", "all"], "[.data[] | select(.target_date==\"${ctx.today}\")] | length") == "2")? + # #84 follow-up: a session completed by an activity from ANOTHER day rendered exactly + # like one completed on time, so the plan quietly implied the work happened on the date + # it was prescribed for. The completing activity's own day is shown when they differ. + # Activity 101 lives on ctx.d1, so target a fixed date it cannot coincide with. + _ = sql!(ctx.db, "INSERT INTO planned_sessions (created_at, target_date, session_type, detail, rationale, status) VALUES ('0','2025-01-15','endurance','early ride','r','open');") + early_id = Str.trim(sql!(ctx.db, "SELECT MAX(id) FROM planned_sessions;")) + _ = stride!(ctx.bin, ctx.home, ["complete", early_id, "101"]) + date_101 = Str.trim(sql!(ctx.db, "SELECT substr(start_local,1,10) FROM activities WHERE id=101;")) + # The control has to be an ON-TIME session checked BY ID. Asserting the output merely + # contains "│ done " is a false positive: it is a prefix of "│ done (Fri ...", so the + # check passed even when every row carried a date. + _ = sql!(ctx.db, "INSERT INTO planned_sessions (created_at, target_date, session_type, detail, rationale, status) VALUES ('0','${date_101}','endurance','same day ride','r','open');") + ontime_id = Str.trim(sql!(ctx.db, "SELECT MAX(id) FROM planned_sessions;")) + _ = stride!(ctx.bin, ctx.home, ["complete", ontime_id, "101"]) + ontime_status = strjq!(ctx, ["plan", "all"], ".data[] | select(.id==${ontime_id}) | .status_shown") + early_status = strjq!(ctx, ["plan", "all"], ".data[] | select(.id==${early_id}) | .status_shown") + # Every assertion selects its OWN row by id. Matching the whole plan output for + # "done (" proved nothing: session 2 is completed with activity 101 earlier in this + # scenario, so that string is already present regardless of what this row renders. + check!("an on-time session renders exactly done", ontime_status == "done")? + check!("the early one carries its real completion date", Str.starts_with(early_status, "done (") and Str.contains(early_status, date_101))? + _ = sql!(ctx.db, "DELETE FROM planned_sessions WHERE id = ${ontime_id};") + _ = sql!(ctx.db, "DELETE FROM planned_sessions WHERE id = ${early_id};") Ok({}) }