Skip to content

Shifts: audience and shift-list options on the coordinator emails - #1792

Merged
peterdrier merged 3 commits into
mainfrom
claude/vibrant-rubin-jmxhe7
Sep 22, 2026
Merged

peterdrier merged 3 commits into
mainfrom
claude/vibrant-rubin-jmxhe7

Conversation

@peterdrier

Copy link
Copy Markdown
Owner

What

Both coordinator email compose forms get an include-shifts checkbox (ticked by default), and the team-wide one gets an audience selection: Upcoming rotas only (default, the previous behaviour) or All rotas in this event, narrowed by Build / Event / Strike (all ticked).

Why

A thank-you after the event needs to reach everyone who held a shift with the department, not just whoever still has one ahead — and it should not carry a schedule. Today the team-wide form silently means "upcoming", and both forms always append the recipient's shift list.

Existing surface checked

  • IRotaCoordinatorMessageService — its three methods took the new options as parameters rather than gaining siblings; BuildTeamRotaGroupsAsync already did the rota-by-rota admission, so the filter slots in there.
  • CoordinatorRotaMessageRequest / CoordinatorTeamRotasMessageRequest — one IncludeShifts flag each rather than a second request shape.
  • ShiftsEmails — one private ShiftSection helper shared by both templates.
  • RotaPeriod (Shifts.Contracts) — reused as the period vocabulary; no new enum.
  • EmailComposerViewModel / _EmailComposer — unchanged; the new controls sit outside it since they are audience, not composition.
  • One new type: TeamRotasAudienceFilter (internal to Shifts). Four loose bools threaded through three methods and a view model read worse, and the UpcomingOnly short-circuit needs one place to live.

UI changes / screenshots

No preview deploy screenshots yet — the PR's own preview at https://{pr_id}.n.burn.camp is the place to check both forms. Rendered markup changes:

  • EmailRota.cshtml: one checkbox below the composer.
  • EmailTeamRotas.cshtml: the same checkbox, a two-radio scope fieldset, a period fieldset hidden under "upcoming", and an "Update recipients" button beside Send.

Checklist

  • Section labeled — Shifts.
  • Targeting main on peterdrier/Humans (the QA fork).
  • Branched off origin/main.
  • Issue refs are qualified — no new issue refs; the doc's existing nobodies-collective/Humans#732 is already qualified.
  • EF migrations — no schema change.
  • NuGet packages updated? — none.
  • New project rule? — none surfaced.
  • Reuse-first checked — see "Existing surface checked" above.
  • Build + test pass locallydotnet build Humans.slnx -v quiet 0 errors; dotnet test Humans.slnx -v quiet green across 49 projects (Integration self-skips, as designed).
  • Nav coverage — no new page; both forms keep their existing entry points and Cancel path.
  • No magic strings — the refresh submit value is EmailTeamRotasViewModel.RefreshIntent, shared by view and controller; resource keys follow the section's existing EmailRota_ / EmailTeamRotas_ / Shifts_Email_ families.
  • Dates/times via NodaTime — unchanged; no new icons.

Other surfaces walked:

  • Cultures — new keys in all six (en, es, de, it, fr, ca), plus the existing team-wide copy that asserted "upcoming" and the two email bodies, whose hardcoded "FYI, your shifts…" lead-in moved into its own key so the section can be dropped whole.
  • Authorization — unchanged; both endpoints still gate on ResolveDepartmentManagementAsync, and the new intent=refresh branch sits after it.
  • Audit — unchanged: one row per dispatch, and the refresh branch writes none because it sends nothing.
  • GDPR — no new personal data; the change only narrows or omits what an existing email already carried.
  • Invariant docsDocs/Shifts.md and Docs/features/email-a-rota.md updated in this PR.

Reviewer notes

  • The trap worth a second look: the Build/Event/Strike boxes are hidden under "upcoming", so an unchecked hidden box would otherwise post false and silently empty an upcoming-scoped audience. TeamRotasAudienceFilter.Includes short-circuits on UpcomingOnly; GetTeamRotasRecipientPreviewAsync_IgnoresPeriodFlags_WhenUpcomingOnly pins it.
  • A RotaPeriod.All rota is admitted by any ticked period. That is a judgment call, not something the existing model dictated.
  • The refresh round-trip is a form POST rather than client-side filtering, so the preview count and the service's audience can never drift. ModelState.Clear() on that branch is what keeps a half-written message from showing a validation error while the coordinator is still picking an audience. formnovalidate handles the client side.
  • intent is a plain action parameter, not a view-model property, so it cannot be smuggled in by model binding from a normal send.

Closes

No issue — raised directly by Peter.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MBDudxvE4SPpBrEP1WFVeb


Generated by Claude Code

Per-rota email (`/Teams/{slug}/Shifts/Rotas/{id}/Email`) gets an
include-shifts checkbox, ticked by default. Cleared, the body drops the
shift section lead-in and all rather than stranding "your shifts are:"
over nothing — so a thank-you reads as one.

Team-wide email (`/Teams/{slug}/Shifts/Email`) gets the same checkbox plus
an audience selection: upcoming rotas only (the previous behaviour, still
the default) or all rotas in the active event, narrowed by Build / Event /
Strike, all ticked. That makes "thank everyone who worked the department"
reachable once the event is over. The period boxes are hidden under
upcoming and `TeamRotasAudienceFilter.Includes` ignores them there, so a
hidden cleared box can never narrow an upcoming send; a `RotaPeriod.All`
rota rides on any ticked period.

Changing an audience control re-posts the form with `intent=refresh` so
the recipient preview and the Send button's count track the selection,
leaving the half-written message and its validation alone. The refresh
button is the no-JS fallback.

Surfaces: six cultures (new keys plus the copy that assumed "upcoming");
authorization unchanged (`ResolveDepartmentManagementAsync`); audit rows
unchanged, one per dispatch; no schema change, so no migration; entry
points unchanged; Shifts.md and the email-a-rota feature doc updated;
tests for the filter, both templates, both dispatch paths and the refresh
branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBDudxvE4SPpBrEP1WFVeb
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-22T15:04:05.053380Z 253e311 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Reviewed commit ed09475 — no issues found.

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown

PR Surface Report

Compared d6e4acce...253e311a. Scored with reforge 0.29.0.

Summary: 23 changed file(s) | EF migrations: 0 added file(s), max 0/1 per context

Reforge Surface Score

metric base head delta
total 20482 20487 +5
surface 16868 16868 0
internal complexity 3614 3619 +5

Section Deltas

section base head delta
Shifts 1645 1650 +5

Section Size & Complexity Deltas

section LOC delta files delta classes delta cognitive p95 delta
Shifts +147 +1 +1 0
Corpus +147 +1 +1 0

Corpus is the solution-wide rollup (same source as Corpus Size & Complexity below), not a sum of the section rows above - cognitive p95 in particular doesn't compose across sections.

Rule Deltas

rule base head delta
longMethod 1671 1675 +4
cognitiveComplexity 862 863 +1

Corpus Size & Complexity

metric base head delta
locProd 186266 186413 +147
methods 6766 6768 +2
classes 2934 2935 +1
files 2170 2171 +1

At head: largest class SurveyService, most complex method DevelopmentDashboardSeeder.SeedAsync.

Published Write Surface

14 of 48 sections publish write capability, 23 interfaces (0).

Interface Surface

No new interfaces or interface methods.

Diff Size

bucket added deleted
code 273 38
tests 302 24
docs 29 6
other 113 35

New Files

  • src/Sections/Humans.Shifts/Services/TeamRotasAudienceFilter.cs
  • tests/Humans.Shifts.Tests/Services/TeamRotasAudienceFilterTests.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed09475b91

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Sections/Humans.Shifts/Views/ShiftAdmin/EmailTeamRotas.cshtml
The audience controls drove the recipient preview through a scripted
re-post, so with the script off or broken a coordinator could widen the
selection and press Send while the form still showed the old count and
names — an irreversible dispatch whose displayed scope was stale.

The form now carries the `TeamRotasAudienceFilter.Key` its recipient list
was built from. A send whose posted selection differs is turned back: the
form re-renders against the new audience with a notice, and sending again
dispatches it. The script is cosmetic now — its failure costs a click,
never a wrong send.

Six cultures for the new notice; authorization, audit and GDPR paths
unchanged (the turn-back sends nothing, so it writes no audit row);
Shifts.md and the email-a-rota doc updated.

Review-round: 1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBz5pZi8nZaweSw8G1agzw

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01adb03010

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Sections/Humans.Shifts/Controllers/ShiftAdminController.cs
The audience guard re-rendered the form after assigning the new
`TeamRotasAudienceFilter.Key` to the model, but the hidden field is an
`asp-for` input and the tag helper prefers ModelState's attempted value
over the model. Model binding had already stored the key the browser
posted, so the re-render emitted the stale one and the next Send posted
it again — turning back forever on exactly the scriptless path the guard
exists to protect. The refresh branch escaped only by way of its
`ModelState.Clear()`.

The posted entry is now dropped where the model's key becomes the
authoritative one, ahead of the assignment rather than inside the guard,
so every return-view path below renders the audience the recipient list
was just built from.

The turn-back test asserted the model property, which was always right;
it now seeds ModelState the way binding does and asserts the entry is
gone — the half the model assertion could not see. Behaviour matches what
Shifts.md and the email-a-rota doc already describe, so neither drifts;
no schema, authorization, audit or localization surface moves.

Review-round: 2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBz5pZi8nZaweSw8G1agzw
@peterdrier
peterdrier merged commit 8049a50 into main Sep 22, 2026
6 checks passed
@peterdrier
peterdrier deleted the claude/vibrant-rubin-jmxhe7 branch September 22, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants