From 7c43b3353010d99eb48bbf08a5110a535242c83a Mon Sep 17 00:00:00 2001 From: Adam Masiarek Date: Sat, 15 Aug 2026 14:06:46 -0400 Subject: [PATCH] fix(emails): count the voters the blast will actually go to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Send 3 Emails" then one email sent, which is what #1287 reports. Both sides apply the same filter; they apply it to different snapshots. The dialog counts `electionRoll`, an array fetched once when the Voters page mounts (`useGetRolls` in a `useEffect` with an empty dependency list, so it is refetched only after editing a voter or clearing the list). The backend re-queries the roll at send time and filters again (`sendEmailController.ts`). Any ballot cast between page load and pressing the button makes the number on the button wrong. Two changes: - Opening "Draft Email Blast" refetches the roll, so the count is of the same rows the backend is about to select. - The button is disabled when the count is zero. Previously "Send N Emails" with a stale N could be pressed against an empty target, and the backend answered with a 400 ("All voters have voted") — an error message for a situation the UI should not have offered in the first place. The reporter suggested dropping the number instead; @ArendPeter's reply says the count is worth keeping for confidence that the filter is right, so this keeps it and makes it true. Not covered here: a roll whose email is blank or undeliverable is still counted and still queued — that is a SendGrid delivery matter rather than a count mismatch, and the issue does not ask about it. --- .../src/components/Election/Admin/SendEmailDialog.tsx | 7 ++++++- .../src/components/Election/Admin/ViewElectionRolls.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/Election/Admin/SendEmailDialog.tsx b/packages/frontend/src/components/Election/Admin/SendEmailDialog.tsx index 04384385d..61b9120ab 100644 --- a/packages/frontend/src/components/Election/Admin/SendEmailDialog.tsx +++ b/packages/frontend/src/components/Election/Admin/SendEmailDialog.tsx @@ -72,12 +72,17 @@ const SendEmailDialog = ({open, onClose, onSubmit, targetedEmail=undefined, elec })) } + // The backend re-queries the roll when it sends, so this count has to be of + // the same thing it will find. The roll is refetched when the dialog is + // opened; before that fix the button could offer to send to voters who had + // already voted since the page was loaded (#1287). const getVoterCount = () => { if(!electionRoll) return 0; if(audience == 'single') return 1; if(audience == 'all') return electionRoll.length; if(audience == 'has_voted') return electionRoll.filter(roll => roll.submitted).length if(audience == 'has_not_voted') return electionRoll.filter(roll => !roll.submitted).length + return 0; } const warning: string = (() => { @@ -174,7 +179,7 @@ const SendEmailDialog = ({open, onClose, onSubmit, targetedEmail=undefined, elec } { setTemplateChosen(false) onSubmit({ diff --git a/packages/frontend/src/components/Election/Admin/ViewElectionRolls.tsx b/packages/frontend/src/components/Election/Admin/ViewElectionRolls.tsx index dd90e0872..6a6f0957c 100644 --- a/packages/frontend/src/components/Election/Admin/ViewElectionRolls.tsx +++ b/packages/frontend/src/components/Election/Admin/ViewElectionRolls.tsx @@ -167,7 +167,7 @@ const ViewElectionRolls = () => { } {usesEmail && - setDialogOpen(true)} sx={{ml: 2}}>Draft Email Blast + { fetchRolls(); setDialogOpen(true); }} sx={{ml: 2}}>Draft Email Blast } {canClearRolls &&