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>
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_onlychanges its redirect target from SiteReport → home. This is the behavioural reversal of Phase A.1's forced redirect.log_journalis new; it does exactly whatlog_onlyused to do (go to the site-journal form for the last-created WorkLog).log_absencesis untouched (Round C).- The conflict-resolution re-render already passes
next_actionthrough via theform.data.itemsloop (Round C, commit8c749f3).log_journalrides 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 alreadyselect_related('project', 'team', 'supervisor')s the work_log, so no new query. Identical query-string construction to the attendance form'slog_absencesbranch (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 successfulform.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)
attendance_logPOSTnext_action=log_only→ 302 to/(home), NOT to/site-report/…. (Regression — this reverses Phase A.1.)attendance_logPOSTnext_action=log_journal→ 302 to/site-report/<id>/edit/.attendance_logPOSTnext_action=log_absences→ 302 to/absences/log/?...(existing test, confirm still green).site_report_editPOST default (next_actionabsent) → saves + 302 to/(home).site_report_editPOSTnext_action=save_absences→ saves + 302 to/absences/log/?date=…&team=…&project=….- Conflict-resolution path carries
next_action=log_journalthrough 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)
/attendance/log/→ submit with Log Work → lands on dashboard with toast, NOT the site-journal form.- Submit with Log Work + Site Journal → lands on the Site Journal form (titled "Site Journal").
- Submit with Log Work + Add Absences → lands on
/absences/log/prefilled (unchanged). - On the Site Journal form, Save Site Journal → dashboard + toast.
- Save Site Journal + Add Absences →
/absences/log/prefilled with the same date/team/project. - Trigger an attendance conflict while having clicked "Log Work + Site Journal" → resolve it → still lands on the Site Journal form.
- 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.