Skip to content

Hours tab on the user detail with void - #47

Merged
byteful merged 5 commits into
mainfrom
feat/user-hours-ledger
Sep 30, 2026
Merged

byteful merged 5 commits into
mainfrom
feat/user-hours-ledger

Conversation

@byteful

@byteful byteful commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

What changed

An operator can open a person's volunteer hours in the admin panel and void an entry with a reason. Admin only.

Before you start

Verify

[Admin]

  1. On the Dashboard, open the "Users" tile, type the citizen's name in "Search name, handle, city…" and click their row
  2. Click the "Hours" tab — Expect: the tab has no count bubble; a strip shows "Total hours", "Entries" and "Voided"; below it one row per credit with the event title, date, place, "Credited by " and the hours as " h"
  3. Click an event row — Expect: the Events page opens with that event selected. Go back to the user.
  4. Click "Void" on a live entry — Expect: a dialog "Void h — " with a "Reason (required)" field; "Void entry" stays disabled while the field is empty
  5. Type a reason and click "Void entry" — Expect: toast " · h voided"; the row turns faded with a red "Voided" pill, the hours struck through, a line "by : ", and no Void button; "Total hours" drops by that amount and "Voided" goes up by one
  6. If the citizen had prepared a transcript that listed that entry — Expect: after the toast a dialog "Issued transcripts still list this entry" lists the transcript code and issue date and only offers "Done"
  7. Click "Void" on another entry and close the dialog with "Cancel" — Expect: nothing changes

[Web] (citizen)

  1. Open your profile and the "Hours" tab — Expect: the voided credit is gone from "Service record" and the total dropped; no notification arrived for the void

[Admin] (account variants)

  1. Open "Hours" on a user with no credits — Expect: "No hours yet" / "This neighbor has no volunteer hours on record."
  2. Open "Hours" on your own operator account — Expect: no "Void" buttons on any row
  3. On a user with more than 20 entries, click "Load more" at the bottom — Expect: older entries append; the button disappears at the end

Regression

User detail tabs — [Admin]

  1. Open a user and click "Reports", "Events" and "Messages" in turn — Expect: each shows its count bubble and its list as before
  2. On "Messages", find a message the person deleted ("[deleted by user]") — Expect: it looks as before (not faded)
  3. Switch to "Hours", then pick another user in "Accounts" — Expect: the new user opens on "Reports"

Confirm and reason dialogs elsewhere — [Admin]

  1. On an active user click "Suspend" — Expect: "Suspend user" with both "Cancel" and "Suspend"; confirming shows " · account suspended"; undo with "Reactivate"
  2. On "Messages" click "Remove" on a message — Expect: "Remove message" with "Reason (optional)"; "Submit" works with the field empty and shows "Message removed"
  3. On the Events page select an upcoming event and click "Cancel event" — Expect: "Cancel this event?" with "Keep event" / "Cancel event"; click "Keep event"
  4. Open "Organizations", pick an org, click "Suspend" — Expect: a "Reason (required)" prompt; "Suspend organization" is disabled until a reason is typed; cancel it

Not covered

  • The operator's own activity trail: the admin panel has no screen that shows audit entries.
  • Revoking an issued transcript is an operator command outside the panel; this plan only checks that the notice names it.

@byteful

byteful commented Sep 27, 2026

Copy link
Copy Markdown
Member Author

@greptileai

@greptile-apps

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[High risk] Dependency bump to a pre-release shared contract version.

Not safe to merge until both dialog failures are fixed and the required size justification is supplied. The event-picker limitation is a separate, non-blocking concern.

Findings

  1. P1 Transcript notice strands confirmation ▶
  2. P1 Transcript codes can disappear ▶
  3. P2 Older matching events are unreachable ▶
  4. P2 Large PR needs justification ▶
Fix with agent prompt
### Issue 1
apps/admin/src/features/users/use-user-hours.ts:102
If a void finishes while a credit confirmation is open, this notice replaces the confirmation in the single-dialog store. The original confirmation never resolves; dismissing the notice leaves the credit form unable to submit or reopen its confirmation. Queue the notice or settle the open dialog first.

### Issue 2
apps/admin/src/components/shared/dialog.tsx:152
When a void affects issued transcripts, `acknowledgeOnly` removes Cancel but still lets Escape, an outside click, or Close dismiss the notice. The caller does not handle dismissal, so the operator can lose the only displayed codes needed to revoke transcripts that still verify with the old total. Keep the notice open until Done or preserve its information after dismissal.

### Issue 3
apps/admin/src/features/users/event-picker.tsx:39-43
The picker displays only eight completed events and offers no next page. With nine events matching the same title and place, the ninth cannot be selected for a credit. The [shared event response](https://github.com/civfix/civfix-app/blob/HEAD/packages/shared/src/schemas/admin/events.ts) provides a cursor, and the [backend list](https://github.com/civfix/civfix-backend/blob/HEAD/services/api/src/services/admin/admin-event-service.ts) returns subsequent pages; expose those pages in the picker. This is a non-blocking limitation for operators crediting older events.

### Issue 4
apps/admin/src/features/users/user-hours-panel.tsx:121
This PR adds roughly 1,500 lines, but its body has no `Size:` justification. That violates the repository directive requiring a `Size:` justification for changes above its 400-line cap. This repository requirement must be satisfied before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1) · Last reviewed commit: "Credit volunteer hours from the admin (#..."

invalidateLedger(qc, id, eventId)
useUiStore.getState().showToast(voidedToast(userName, hours))
if (res.affectedCertificates.length === 0) return
void confirmDialog({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Transcript notice strands confirmation

If a void finishes while a credit confirmation is open, this notice replaces the confirmation in the single-dialog store. The original confirmation never resolves; dismissing the notice leaves the credit form unable to submit or reopen its confirmation. Queue the notice or settle the open dialog first.

Knowledge Base Used: Application shell and navigation

Artifacts

Executed hours-panel UI harness

  • This harness mounted the real hours panel and dialog host with controlled API responses, enabling the overlapping-dialog test.

Executed Chromium reproduction script

  • This script drove both void-response conditions through the UI and asserted their credit API call counts, reproducing the failure.

▶ Credit confirmation remains available without affected certificates

  • Chromium completed a pending void without affected certificates, then accepted the still-open credit confirmation and made one credit call.

Credit confirmation after a void with no affected certificates

  • The captured frame shows the credit confirmation still available after the void response, so the operator can proceed.

▶ Certificate notice replaces credit confirmation

  • Chromium completed the same pending void with an affected certificate, then dismissed the replacement notice and found that retrying made no credit call.

Certificate notice occupying the credit confirmation

  • The captured frame shows the transcript notice where the credit confirmation had been, making the overwritten dialog visible.

Chromium reproduction output

  • The captured command output records exit code 0, one credit call in the control run, and zero calls after the certificate notice.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/features/users/use-user-hours.ts
Line: 102

Comment:
**Transcript notice strands confirmation**

If a void finishes while a credit confirmation is open, this notice replaces the confirmation in the single-dialog store. The original confirmation never resolves; dismissing the notice leaves the credit form unable to submit or reopen its confirmation. Queue the notice or settle the open dialog first.

**Knowledge Base Used:** [Application shell and navigation](https://app.greptile.com/civfix/-/custom-context/knowledge-base/civfix/civfix-admin/-/docs/application-shell-and-navigation.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

<button className="btn ghost" onClick={cancel}>
{current.kind === "confirm" ? (current.cancelLabel ?? "Cancel") : "Cancel"}
</button>
{!(current.kind === "confirm" && current.acknowledgeOnly) && (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Transcript codes can disappear

When a void affects issued transcripts, acknowledgeOnly removes Cancel but still lets Escape, an outside click, or Close dismiss the notice. The caller does not handle dismissal, so the operator can lose the only displayed codes needed to revoke transcripts that still verify with the old total. Keep the notice open until Done or preserve its information after dismissal.

Artifacts

Chromium transcript-notice validation script

  • The authored script bundles the dialog and void-success callback, runs both comparison conditions, and asserts the observed dismissal behavior.

Chromium validation command and observed results

  • The executed command exited 0 and recorded that all three dismissal routes stayed blocked in the control but dismissed the current-source notice.

▶ Guarded comparison keeps the transcript notice open

  • Chromium exercised Escape, an outside click, and header Close against an in-memory guarded control; the notice remained until Done was selected.

Guarded comparison with affected transcript codes visible

  • A frame captured during the guarded-control run shows the notice and its two affected codes.

▶ Current source dismisses the transcript notice without Done

  • Chromium exercised the unchanged source and showed Escape, an outside click, and header Close each dismissing the notice without Done.

Current-source notice with affected transcript codes visible

  • A frame captured during the current-source run shows the notice before its dismissal routes were exercised.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/components/shared/dialog.tsx
Line: 152

Comment:
**Transcript codes can disappear**

When a void affects issued transcripts, `acknowledgeOnly` removes Cancel but still lets Escape, an outside click, or Close dismiss the notice. The caller does not handle dismissal, so the operator can lose the only displayed codes needed to revoke transcripts that still verify with the old total. Keep the notice open until Done or preserve its information after dismissal.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +39 to +43
const list = useEventList(
{ q: q === "" ? undefined : q, filter: "completed", limit: RESULT_LIMIT },
{ keepPreviousData: true },
)
const results = list.data?.items ?? []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Older matching events are unreachable

The picker displays only eight completed events and offers no next page. With nine events matching the same title and place, the ninth cannot be selected for a credit. The shared event response provides a cursor, and the backend list returns subsequent pages; expose those pages in the picker. This is a non-blocking limitation for operators crediting older events.

Knowledge Base Used: Event management

Artifacts

Executed Chromium event-picker repro script

  • The authored command bundles the real picker, serves nine matching events, and runs baseline and pagination-overlay browser checks, making the test reproducible.

Rendered event-picker harness source

  • The generated entry mounts EventPicker with a query client and displays the selected event ID, showing what the executed browser script rendered.

Successful picker validation run and observed API requests

  • Running `node trex-artifacts/event-picker-pagination-repro.cjs` from `/home/user/repo` exited 0 and recorded no baseline cursor request versus a successful cursor request and selection in the comparison.

▶ Unchanged picker with the ninth matching event unreachable

  • Chromium searched and scrolled the real picker with nine identical-search completed events; only the first eight rows were available.

Baseline picker after scrolling its eight results

  • Playwright captured the unchanged picker after scrolling its result list, with no pagination control present.

▶ Pagination-overlay picker selects the ninth event

  • Chromium used an in-memory pagination overlay, loaded the cursor page, and selected `event-9`, showing the comparison behavior without changing tracked code.

Comparison picker after selecting event nine

  • Playwright captured the pagination-overlay picker after selecting the oldest event, confirming the selection state.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/features/users/event-picker.tsx
Line: 39-43

Comment:
**Older matching events are unreachable**

The picker displays only eight completed events and offers no next page. With nine events matching the same title and place, the ninth cannot be selected for a credit. The [shared event response](https://github.com/civfix/civfix-app/blob/HEAD/packages/shared/src/schemas/admin/events.ts) provides a cursor, and the [backend list](https://github.com/civfix/civfix-backend/blob/HEAD/services/api/src/services/admin/admin-event-service.ts) returns subsequent pages; expose those pages in the picker. This is a non-blocking limitation for operators crediting older events.

**Knowledge Base Used:** [Event management](https://app.greptile.com/civfix/-/custom-context/knowledge-base/civfix/civfix-admin/-/docs/event-management.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

<Icons.X size={11} /> Void
</button>
)}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Large PR needs justification

This PR adds roughly 1,500 lines, but its body has no Size: justification. That violates the repository directive requiring a Size: justification for changes above its 400-line cap. This repository requirement must be satisfied before merging.

Rule Used: # civfix review rules civfix is a live civic-tech platform that will hold government contracts. Review every PR for correctness, security and performance. Flag real defects with evidence; skip style nits that lint already covers. ## Repos - **... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/features/users/user-hours-panel.tsx
Line: 121

Comment:
**Large PR needs justification**

This PR adds roughly 1,500 lines, but its body has no `Size:` justification. That violates the repository directive requiring a `Size:` justification for changes above its 400-line cap. This repository requirement must be satisfied before merging.

**Rule Used:** # civfix review rules  civfix is a live civic-tech platform that will hold government contracts. Review every PR for **correctness, security and performance**. Flag real defects with evidence; skip style nits that lint already covers.  ## Repos  - **... ([source](https://app.greptile.com/civfix/-/custom-context?memory=39a53925-3d93-4c82-980e-27b67393717d))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@greptile-apps

greptile-apps Bot commented Sep 27, 2026

Copy link
Copy Markdown

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P1 Transcript notice strands an open credit confirmation ▶

    • Bug
      • When a void succeeds while the credit confirmation is open, its affected-certificate notice takes the dialog's place. Dismissing the notice does not restore or settle the credit confirmation, so the pending submission cannot proceed.
    • Cause
      • The void mutation calls confirmDialog at use-user-hours.ts:102, while the dialog store has only one current request. Opening the notice overwrites the credit request and its resolver; the credit form remains guarded while its awaited promise is pending.
    • Fix
      • Queue dialog requests or otherwise preserve and settle the existing confirmation before showing the transcript notice.
  • P1 Acknowledge-only transcript notice can be dismissed without acknowledgement ▶

    • Bug
      • After a void affects issued transcripts, Escape, an outside click, and header Close each remove the notice despite its missing Cancel button. The operator can lose sight of the codes and revocation command without choosing Done; the caller discards the dialog result.
    • Cause
      • The acknowledgeOnly condition at dialog.tsx:152 controls only the Cancel button. Escape, overlay, and header Close still invoke cancel(), while use-user-hours.ts invokes void confirmDialog(...).
    • Fix
      • Prevent those three dismissal routes for acknowledge-only confirms, leaving Done as the dismissal action; alternatively, explicitly handle a negative result and preserve the revocation information.
  • P2 Event picker cannot reach a ninth completed event with the same search terms ▶

    • Bug
      • When more than eight completed events match a title/place search, an older event beyond the first page has no selectable row or pagination control. In the nine-event repro, event-9 remained unselectable.
    • Cause
      • EventPicker requests limit: 8 through the single-page useEventList hook and renders only data.items, ignoring the response's nextCursor.
    • Fix
      • Use cursor pagination in the picker, retain rows from fetched pages, and expose a Load more control when another page exists.

@byteful
byteful force-pushed the feat/user-hours-ledger branch from 430ef93 to cf2c9a2 Compare September 30, 2026 00:56
@byteful
byteful merged commit 66015e5 into main Sep 30, 2026
1 check passed
@byteful
byteful deleted the feat/user-hours-ledger branch September 30, 2026 00:57
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.

1 participant