38686-vm/docs/plans/2026-05-15-post-attendance-flow-v2-design.md
Konrad du Plessis 110545b11e 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>
2026-05-15 13:02:18 +02:00

10 KiB

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:

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.