Skip to content

CE sign-in/out tracking + attendance report - #2075

Open
maebeale wants to merge 26 commits into
mainfrom
maebeale/ce-attendance-report
Open

CE sign-in/out tracking + attendance report#2075
maebeale wants to merge 26 commits into
mainfrom
maebeale/ce-attendance-report

Conversation

@maebeale

@maebeale maebeale commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 new model + migration, public sign-in/out actions, an inline editor with its own write path, a certificate gate, and a per-event report across the CE callout + participation/registrants surfaces

Replaces AWBW's paper CE-hour sign-in sheet with in-portal sign-in/out, kept invisible to the general training group — surfaced only on each registrant's private CE callout, and only once CE is paid in full.

Why

  • CE hours must live in a durable, auditable system of record for the CE-accrediting body; a shared Google sheet isn't that.
  • Staff need one screen to see who's signed in/out during a training to chase reminders — and to fix a missed sign-in without leaving it.

What

  • EventAttendanceTimeEntry — generic per-day sign-in/out pairs (many/day for breaks), audited by created_by/updated_by. Event#attendance_sign_in_open? derives a per-day window from the event's single start/end time-of-day. Guards reject >24h logged on a day and any same-day overlap (checking persisted rows + the in-memory nested batch, so self-service and the edit forms are all covered).
  • CE callout — one button by state: Sign in (inside the day's window) / Sign out (any time that day, while an entry is open). Today's entries + running total render below; the callout card shows a live nudge mirroring the payment-due badge. The whole section drops once the last day's sign-in window has passed.
  • Catch-up sign-out — a day left open gets its own amber prompt on the callout, naming the day and the time it will record: that day's scheduled end, never "now". Kept separate from today's sign-in/out (its own ?entry_id) so closing one can't be mistaken for the other. Not offered when the sign-in lands after that day already ended — nothing sensible to stamp, so staff correct it on the report.
  • EventAttendanceReport — per event, grouped by day then registrant. Leads with a teal Totals table (Day N logged columns, per-day + hours-awarded totals in the All row); a registrant's Total logged is scoped to the event's days so it always equals its day columns. ?ce=true scopes to CE registrants and adds license # / awarded hours. Reached from CE-eligible events on the participation report and the registrants bulk-actions dropdown.
  • Inline day editor on the report — each day's sessions cell opens in place (?edit=<cell>event_registrations#update_attendance). Clock times, not datetimes, since the day is the section it sits in; blank trailing row adds a session (including both halves of a day nobody signed in on), Remove drops one, blank sign-out leaves it open. A rejected save reopens the cell prefilled with what was typed. EventAttendanceEntriesUpdate holds the nested-attributes write + audit stamping, shared with the CE edit form.
  • Certificate gate — when attendance was tracked, the CE certificate also requires logged time to cover ~90% of the awarded contact hours; events that never tracked time are unaffected (no retroactive blocking).

Notes

  • Self-service sign-in/out is public and unattributed (created_by nil); only staff edits are attributed.
  • Sign-out isn't time-boxed within a day, but doesn't cross days. A forgotten sign-out can be closed any time that day; overnight it stops counting as "signed in", so it can't block the next day's sign-in or bank a ~24h session against the previous one. It gets the catch-up button instead, and stays flagged signed in on that day's report row.
  • The inline editor deliberately can't express a session crossing midnight (clock times against one day) — the CE edit page keeps full datetime rows for that.
  • Entry validation messages are written as whole sentences and shown verbatim; ApplicationController#error_sentence stops nested attributes pasting the humanized association name in front of them.
  • Events store only one start/end datetime, so multi-day windows assume each day runs the same hours (same assumption day_count already makes). Times use the request zone (Pacific), matching the paper sheet.
  • Rebased on main (incl. CE callout: surface fee/deadline; show pending certificate card #2073's CE-card rework — the live sign-in nudge composes with its new fee/deadline/request-card logic).

Tests

Model/service/request/policy/view specs across every part, including the two entry guards, the day-boundary behaviour of an entry left open, the catch-up close (and when it's declined), the per-day scoping of the signed in flag, every inline-editor path (add/correct/remove/leave-open/rejected-save/bad-date/authz), and total-vs-column consistency. Feature suite green.

Copilot AI lite review requested due to automatic review settings August 4, 2026 00:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 00:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 00:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 00:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 00:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale force-pushed the maebeale/ce-attendance-report branch from d85eec8 to 2e34c52 Compare August 4, 2026 03:30
Copilot AI review requested due to automatic review settings August 4, 2026 03:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 05:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 05:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 05:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 12:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 12:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 12:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 13:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 13:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 13:06
maebeale and others added 21 commits August 6, 2026 12:03
EventAttendanceReport groups a training's sign-in/out entries by day then
registrant, with per-day and grand-total minutes — the in-portal CE hour
sign-in sheet. `?ce=true` scopes to CE registrants and shows license number and
awarded hours; the generic view covers anyone who logged time. Reached at
attendance_event_path (dashboard-level auth) and linked from CE-eligible events
on the participation report. Also shows a "X of Y signed in" nudge for chasing
sign-ins during the training.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When attendance time has been tracked for a CE registrant, the certificate now
also requires the logged minutes to cover ~90% of the awarded contact hours — you
can't certify hours the sign-in sheet doesn't support. Events that never tracked
time (no entries) are unaffected: day-level attendance alone still governs, so this
never retroactively blocks existing certificates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "CE sign-in report" entry to the registrants bulk-actions dropdown (CE
events only), returning to the registrants page. On the report, each CE registrant
gets an Edit link to their CE edit page. Report exposes ce_registration_for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CE edit opened from the sign-in report now returns there (eyebrow,
  Cancel, and after-save/destroy redirects all honor return_to=attendance,
  anchored to the totals section)
- The report warns when an event outruns event_dates' 5-day cap, so
  missing day sections aren't mistaken for missing data
- Drop the redundant single-column FK index (the composite covers it,
  declared in-table so MySQL doesn't auto-create one) and the unused
  EventAttendanceReport#any_entries?

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A stale remove (a second tab, browser-back on the turbo:false form, or a
double-submit) sent a _destroy for an entry that was already gone, so nested
attributes raised RecordNotFound and 500'd the save. Drop rows pointing at
entries no longer on the registration so a stale remove is a quiet no-op.

Also: one blank add-row instead of three (only saved entries get a Remove box,
so three empties read as broken); the three form sections were touching, so
wrap them in space-y-6; and when the sign-in window isn't open, show a standard
gating notice naming the concrete opening (event zone, labeled) plus the rule,
and hide the arrive/leave hint until sign-in is actually usable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the all-days totals above the per-day sections and give them a teal
treatment so the headline CE figures read first, with Day 1 / Day 2 below under
a "By day" heading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reject an entry (or nested batch) that pushes a day's logged time past 24 hours,
or that overlaps another sign-in on the same day — you can't be signed in twice
at once. Cross-entry checks read persisted rows plus the in-memory nested batch,
so both self-service sign-in and the CE edit form are covered; self-service now
redirects with the error instead of raising.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e links

Add Day N logged columns (and per-day + hours-awarded totals in the All row) to
the Totals table. Scope a registrant's Total logged to the event's days so it
always equals the day columns — time logged on non-event dates no longer inflates
it invisibly. Each row now links to the CE edit page and the name to that
registrant's CE callout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Put the "… logged" / "awarded" qualifier on a second line in smaller lowercase
text, so the day columns stop crowding into one hard-to-read caps line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Staff reading the sign-in sheet need the day's expected hours next to the
logged times to spot short days at a glance — reuse the event decorator's
times in the page header and each day header. Also tighten the CE callout's
sign-in-window note ("30 minutes before."), since the concrete opening
time already says when.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Callout page header: event dates move to their own line (the combined
  title · date line wrapped awkwardly) and gain the daily hours
- The pre-window CE note names the opening time and, on its own line, the
  event's actual start ("Event begins 30 min later, at 9:00 PDT") — a
  countdown was tried and cut in favor of the concrete times

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Signed out" implied a session that never happened for first-time
visitors — show only the Sign in button until an entry exists today.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- A sub-minute in/out pair counted as 0m; round up in the attendee's
  favor so every completed session logs at least a minute
- Label the today's-entries columns with the display zone ("Time in
  (PDT)") since bare clock times were ambiguous
- Sign out gets the primary CTA treatment while signed in (it's the only
  action), and the button reads "Sign in again" once a session exists

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stacked label/value rows made the card tall and left the status chip
stranded mid-card — status now sits on the heading row, and the credit
stats (hours at the same scale as cost) and license fields each share
one wrapping row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Staff viewing a registrant's CE page had no direct path to the event-wide
report; reuse the admin_edit_link chip (gated on the attendance? policy)
beside the Training sign-in heading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Session detail now defaults to grouped-by-person (one card per
  registrant with a row per day), with a registrants-style pill toggle to
  the by-day tables — staff usually chase one person's hours, not a day
- Non-CE events get the report too: participation rows and the
  registrants bulk-actions menu link a generic "Sign-in report" where the
  CE-scoped link doesn't apply

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rows in both report groupings now open the CE edit page on the CE report
and the registration edit page on the generic one — with return_to
wiring (eyebrow + after-save redirect) added to the registration edit
page so the round trip lands back on the report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Forgetting to sign out is the failure mode this feature plans for, but an
open entry had no date, so it carried into the next day of a multi-day
training: sign-in was refused as "already signed in", and the only way out
banked a ~24h session against the previous day (or tripped the daily limit
and stranded the registrant entirely). Open entries are now scoped to the
day being asked about, so each day starts fresh and the stale row stays
flagged on the attendance report for staff to close.

Two smaller fixes from the same review:

- Drop the sign-in section once the last day's window has passed, instead
  of telling a registrant collecting their certificate that "Sign-in opens
  30 minutes before" a training that's already over.
- Render the staff time-entry rows from the in-memory association, so a
  save rejected for overlapping or backwards times re-renders with the
  admin's typed values instead of silently reverting them.
Review follow-ups on the attendance work:

A forgotten sign-out previously sat open until staff noticed it on the
report, which also under-counted the registrant's hours against the CE
certificate gate. It now gets its own catch-up button on the callout,
stamped with that day's scheduled end rather than "now" — the correction
staff made on the paper sheet — kept separate from today's sign-in/out so
the two days can't be confused.

The report's "signed in" chip was computed across every entry on the
registration but rendered inside a per-day row, so one forgotten sign-out
lit up every later day too — the opposite of what staff scan the report
for. Scope it to the day.

Attendance-entry validation messages reached the admin through the parent
registration's nested attributes, which pasted the humanized association
name in front of sentences written to display verbatim ("Event attendance
time entries This sign-in overlaps…").

The report's registrant-name links led to a callout whose eyebrow returned
to the CE edit page, leaving no way back to the report.

Co-Authored-By: Claude <noreply@anthropic.com>
Fixing one missed sign-in meant leaving the attendance report for the CE
edit page, correcting a datetime, and navigating back — for a sheet whose
whole point is scanning a training day at a glance.

Each day's sessions cell now opens in place. Times are clock times, not
datetimes: the day is the section the editor sits in, so a correction is two
fields rather than two dates to retype and get wrong. A blank trailing row
adds a session (including both halves of a day nobody signed in on), Remove
drops one, and an empty sign-out leaves the session open. Registrants still
stamp their own times from the CE callout — this is the correction surface,
not a replacement for it.

Server-rendered via an `edit` param rather than a Stimulus toggle, so a
rejected save can hand the submitted times back through the flash and reopen
the cell with them; the CE edit page keeps that property and the report
shouldn't lose it. The whole-row link is suppressed while a cell is being
edited so a stray click can't navigate away mid-correction.

The nested-attributes write and audit stamping now live in
EventAttendanceEntriesUpdate, shared with the CE edit form, and the
verbatim-message error formatting moves to ApplicationController since two
controllers now surface nested-association failures.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 16:50
@maebeale
maebeale force-pushed the maebeale/ce-attendance-report branch from 622ce96 to b7dfb14 Compare August 6, 2026 16:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The CE-report name links once pointed the CE page eyebrow at
return_to=ce_registration, stranding admins one hop from the report;
assert the attendance value (and the old value's absence) so the
eyebrow chain can't silently regress.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 7, 2026 12:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Both the sign-in-opening and event-begins times already show AM/PM; align the
event-begins hour directive (%-l → %-I) so the adjacent times format identically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 7, 2026 12:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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