docs: post-attendance flow v2 design

Replaces the forced post-attendance SiteReport redirect with 3
explicit buttons (Log Work → dashboard / + Site Journal / +
Absences) + a parallel "Save Site Journal + Add Absences" on the
journal page. Renames the user-facing "Site Report" → "Site
Journal" (display-only, Path-A; frees "Journal" for the parked
voice feature). Reuses the Round C next_action POST mechanism —
no model/migration/URL changes. NOT to be deployed until Konrad
verifies locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-05-15 13:02:18 +02:00
parent d7015b9210
commit 110545b11e

View File

@ -0,0 +1,203 @@
# Post-Attendance Flow v2 — Design
**Date:** 15 May 2026
**Status:** Approved by Konrad on 15 May 2026; ready for implementation plan.
**Branch:** `ai-dev`. **NOT to be pushed/deployed until Konrad confirms it works locally.**
## Goal (one sentence)
Replace the forced auto-redirect into the Site Report form after every
attendance submit with three explicit, clearly-prioritised choices —
"just log work", "log work + site journal", "log work + absences" — and
add a parallel "save + add absences" path on the site-journal page,
while renaming the user-facing vocabulary to "Site Journal" to free up
"Journal" for the (parked) voice-transcript feature.
## Why
Phase A.1 shipped a two-step flow: attendance submit → forced redirect
to the SiteReport form (with a Skip link). In real use Konrad found the
forced redirect intrusive — most days he just wants to log attendance
and be done. The fix is to make the site-journal step (and the absences
step) explicit opt-in buttons rather than a mandatory interstitial.
## Decisions locked in (from the brainstorm)
| # | Question | Decision |
|---|----------|----------|
| 1 | "Journal" naming collision (parked voice JournalEntry vs the structured SiteReport form) | Rename the SiteReport-the-page to **"Site Journal"** in all UI text. Model/view/URL stay `SiteReport`/`site_report_*` in code (Path-A display-only rename, zero migration). The parked voice feature will be renamed (e.g. "Voice Notes") so it doesn't collide. |
| 2 | Where does plain "Log Work" land now? | **Dashboard** (home) + the existing green "work log(s) created" toast. No more forced SiteReport redirect. |
| 3 | Button structure | **Approach C** — exactly the 3 explicit buttons Konrad asked for, but with deliberate visual hierarchy (primary "Log Work" + two secondary "+ …" buttons) so 3 buttons don't feel like a wall. |
| 4 | End of the "+ Add Absences" chain | The absences form's own buttons (Log Absences → list, Cancel) are the natural end. No extra chaining. |
## § 1 — Vocabulary rename (display-only, zero migration)
User-facing text changes from "Site Report" / "Log Today's Work" →
**"Site Journal"**:
- `core/templates/core/site_report_edit.html``<h1>` "Log Today's
Work" → "Site Journal"; save button "Save Site Report" → "Save Site
Journal"; `{% block title %}` text.
- `core/templates/core/site_report_detail.html` — heading + `<title>`.
- `core/templates/core/work_history.html` — the clipboard-icon tooltip
/ link title text that references "site report".
- Success-toast string in `core/views.py::site_report_edit` — "Site
report saved …" → "Site journal saved …".
**Unchanged in code (Path-A pattern, per CLAUDE.md "UI-vs-DB naming
drift"):** `SiteReport` model, `site_report_edit` /
`site_report_detail` views, `/site-report/<id>/edit/` + `/site-report/
<id>/` URLs, the `work_log.site_report` related-name, `SiteReportForm`,
`core/site_report_schema.py`, all existing tests. Only user-visible
strings move.
**Parked-work doc note:** add a line under the Backburner section that
the future voice-transcript feature must NOT be called "Journal" (the
name is now taken by the site-progress form) — suggest "Voice Notes".
## § 2 — Attendance form: 3 buttons, clear hierarchy
`/attendance/log/` submit block (top → bottom, Konrad's specified order):
| Button label | Bootstrap style | `next_action` value | Redirect on success |
|---|---|---|---|
| **Log Work** | `btn btn-lg btn-accent` (primary, orange) | `log_only` | `redirect('home')` + toast |
| **Log Work + Site Journal** | `btn btn-lg btn-outline-secondary` | `log_journal` *(NEW)* | `redirect('site_report_edit', work_log_id=<last created>)` |
| **Log Work + Add Absences** | `btn btn-lg btn-outline-secondary` | `log_absences` | `/absences/log/?date=&team=&project=` (unchanged) |
- The primary orange "Log Work" anchors the eye — the common case.
- The two `+ …` buttons are visually secondary (outline) — opt-in.
- `log_only` changes its redirect target from SiteReport → home.
This is the behavioural reversal of Phase A.1's forced redirect.
- `log_journal` is new; it does exactly what `log_only` used to do
(go to the site-journal form for the last-created WorkLog).
- `log_absences` is untouched (Round C).
- The conflict-resolution re-render already passes `next_action`
through via the `form.data.items` loop (Round C, commit `8c749f3`).
`log_journal` rides those same rails for free — no extra work, but
needs a regression test.
## § 3 — Site Journal page: 3 actions, same hierarchy
`/site-report/<id>/edit/` action row:
| Action label | Style | Behaviour |
|---|---|---|
| **Skip** | quiet `btn btn-outline-secondary` (or text link) | → `home`, nothing saved (unchanged) |
| **Save Site Journal** | `btn btn-accent` (primary) | Save → `home` + toast (unchanged destination) |
| **Save Site Journal + Add Absences** | `btn btn-outline-secondary` | Save → `/absences/log/?date=&team=&project=` prefilled from the WorkLog |
- Mirror of the attendance form's hierarchy.
- The absence prefill query string is built from `work_log.date`,
`work_log.team_id`, `work_log.project_id` — the view already
`select_related('project', 'team', 'supervisor')`s the work_log, so
no new query. Identical query-string construction to the attendance
form's `log_absences` branch (DRY: consider a tiny shared helper
`_absence_prefill_qs(work_log)` but YAGNI — two call sites, ~3 lines
each; decide during implementation).
- New submit button carries `name="next_action" value="save_absences"`.
The view reads it after a successful `form.save()`.
## § 4 — View changes
### `core/views.py::attendance_log` (POST success branch)
Current Round C logic: `next_action == 'log_absences'` → absences
prefill; else → `redirect('site_report_edit', …)`.
New logic:
- `next_action == 'log_absences'` → absences prefill (unchanged)
- `next_action == 'log_journal'` → `redirect('site_report_edit',
work_log_id=created_log_ids[-1])` (what the old default did)
- else (`log_only`, missing, or anything unrecognised) →
`redirect('home')` + success toast
### `core/views.py::site_report_edit` (POST success branch)
Current: always `redirect('home')` after `instance.save()`.
New:
```python
instance.save()
messages.success(request, f"Site journal saved for {work_log.project.name} on {work_log.date:%d %b %Y}.")
if request.POST.get('next_action') == 'save_absences':
from urllib.parse import urlencode
params = {'date': work_log.date.isoformat()}
if work_log.team_id:
params['team'] = work_log.team_id
if work_log.project_id:
params['project'] = work_log.project_id
return redirect(f"{reverse('absence_log')}?{urlencode(params)}")
return redirect('home')
```
(Exact variable names verified against the live view during planning.)
## Tests (~5, in `core/tests.py`)
1. `attendance_log` POST `next_action=log_only` → 302 to `/` (home),
NOT to `/site-report/…`. (Regression — this reverses Phase A.1.)
2. `attendance_log` POST `next_action=log_journal` → 302 to
`/site-report/<id>/edit/`.
3. `attendance_log` POST `next_action=log_absences` → 302 to
`/absences/log/?...` (existing test, confirm still green).
4. `site_report_edit` POST default (`next_action` absent) → saves +
302 to `/` (home).
5. `site_report_edit` POST `next_action=save_absences` → saves +
302 to `/absences/log/?date=…&team=…&project=…`.
6. Conflict-resolution path carries `next_action=log_journal` through
to the final redirect (extend the existing Round C conflict test).
Update any existing test that asserted attendance submit redirects to
the site report by default — that contract is intentionally changing.
## Files touched
| File | Change |
|---|---|
| `core/templates/core/attendance_log.html` | +1 button (`log_journal`), restyle 3 buttons into primary/secondary hierarchy |
| `core/templates/core/site_report_edit.html` | text → "Site Journal"; +1 submit button (`save_absences`) |
| `core/templates/core/site_report_detail.html` | heading/title text → "Site Journal" |
| `core/templates/core/work_history.html` | clipboard tooltip text → "site journal" |
| `core/views.py::attendance_log` | 3-way `next_action` branch (log_only→home, log_journal→site report, log_absences→absences) |
| `core/views.py::site_report_edit` | `next_action=save_absences` branch + toast text |
| `core/tests.py` | ~6 new/updated tests |
| `docs/plans/parked-work.md` | Backburner note: future voice feature ≠ "Journal" |
| `CLAUDE.md` | Update the SiteReport line + URL-routes note: UI label is "Site Journal", model stays `SiteReport` (another Path-A entry) |
**No model / migration / URL / dependency changes.** ~120 LOC incl.
tests. Pure flow + display change.
## Out of scope (deliberately)
- No change to the SiteReport model, schema, or `site_report_schema.py`.
- No change to the absences feature itself (just the prefill entry point).
- No actual building of the voice-transcript "Voice Notes" feature —
still backburnered; this only reserves the name.
- No "what next?" interstitial panel (rejected in Q2 — Konrad chose a
plain dashboard landing for `log_only`).
## Verification (manual, local — Konrad)
1. `/attendance/log/` → submit with **Log Work** → lands on dashboard
with toast, NOT the site-journal form.
2. Submit with **Log Work + Site Journal** → lands on the Site Journal
form (titled "Site Journal").
3. Submit with **Log Work + Add Absences** → lands on `/absences/log/`
prefilled (unchanged).
4. On the Site Journal form, **Save Site Journal** → dashboard + toast.
5. **Save Site Journal + Add Absences**`/absences/log/` prefilled
with the same date/team/project.
6. Trigger an attendance conflict while having clicked "Log Work +
Site Journal" → resolve it → still lands on the Site Journal form.
7. Full suite green (`USE_SQLITE=true … manage.py test core.tests`).
Then — and only then — Konrad decides whether to push to `ai-dev` +
deploy.
## Branch / deploy
Build on `ai-dev`. **Do NOT push to origin or deploy** until Konrad
has run the local verification above and explicitly approves. This is
a UX-flow change to a daily-use path — it warrants a hands-on local
check before it reaches production.