Extract email composer into reusable component for payment reminders#34
Open
fordprefect480 wants to merge 5 commits into
Open
Extract email composer into reusable component for payment reminders#34fordprefect480 wants to merge 5 commits into
fordprefect480 wants to merge 5 commits into
Conversation
Admins can now remind members who haven't paid their membership directly from the Members page. A "Send payment reminder" action lets them pick one of two cohorts — members not yet paid for the upcoming financial year, or members whose membership has fully lapsed — and jumps to the email composer with those recipients and a pre-filled, editable draft. - Server: GET /api/admin/members/membership-renewal exposes the renewal target date (next 1 July boundary, with the existing late-join carry-over, computed in Adelaide local time) so the browser can identify who hasn't paid for the upcoming year without re-deriving the boundary client-side. - Members page: cohort selection panel with live counts, skipping members without an email address. - Email composer: accepts pre-selected recipients and a draft via router state; surfaces a note so the admin reviews the list before sending. Reuses the existing newsletter "specific members" send pipeline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FZmX5BKggFcrFTKk7BfEv
Replace the cohort-button reminder flow with a more direct, transparent UX on the Members page: select rows with checkboxes, narrow with the filter chips, then email the selection from a modal. - Extract the composer into a shared component (emailComposer.tsx): HtmlEditor, template split/load (useEmailTemplate), isBodyEmpty, and ComposeForm (subject + body + send/confirm/error pipeline). Both the full-page composer and the new modal use it, so behaviour stays identical with no duplication. EmailCompose keeps its three recipient modes and now renders ComposeForm. - PaymentReminderModal: pre-filled renewal draft, removable recipient chips, skips selected members without an email, sends via the existing newsletter "specific members" path, and lands on the sent-email detail for delivery feedback. - Members page: checkbox column with a select-all-(filtered) header checkbox (indeterminate for partial); selection tracked by id so it survives filter changes; checkbox clicks don't trigger row navigation. A selection toolbar exposes "Email selected". - Retarget "Paid"/"Not Yet Paid" to the upcoming renewal boundary (ComputePaidThrough) instead of "now", so the chip singles out members who haven't paid for the year ahead. For consistency the member detail pill and the Dashboard's member-stats count use the same definition. - Simplify paymentReminder.ts to a single buildReminderDraft helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FZmX5BKggFcrFTKk7BfEv
Fix membership status so a member covered through to the end of the upcoming
financial year is shown as paid for it. The check compared paid-through
against the year's END boundary (1 July) with >=, so a member covered until
30 June read as "not yet paid" for that very year - the detail card then
contradicted itself ("covered until 30 June 2027" yet "not paid for 2026/27").
Now coverage is tested against the START of the renewal year (shared
isPaidForRenewalYear helper), which is robust to the off-by-hours boundary and
still flags members covered only for the prior year. Applied consistently to
the Members list chips, the member-detail pill, and the Dashboard stat.
Generalise the selected-members modal: it's no longer payment-specific.
Renamed PaymentReminderModal -> MemberEmailModal, titled "Email selected
members", and it starts blank so the admin can send anything. A one-click
"Insert membership-reminder template" button keeps the renewal-reminder case
easy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012FZmX5BKggFcrFTKk7BfEv
Modal: drop the "Insert reminder template" button and seed the body with a plain "Hi," greeting starter instead, so it's a general email-to-selected- members tool. Removes the now-unused paymentReminder.ts draft helper and the config props/wiring it needed. Tests: - Extract the membership "paid for the renewal year" rule into pure, testable MembershipPeriod.RenewalYearStart / IsPaidForRenewalYear methods (GetMemberStatsEndpoint now uses RenewalYearStart for its threshold), and add xunit coverage for the boundary cases - including the regression where a member covered to the last day before the 1 July boundary must read as paid. - Add a frontend test runner (vitest, `npm test`) and cover the frontend side of the same logic: isPaidForRenewalYear, financialYearLabel, and membershipPaidThroughFyLabel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FZmX5BKggFcrFTKk7BfEv
Stand up a WebApplicationFactory-based integration harness for the API: - TestApiFactory boots the real app under a new "Testing" environment, swaps the SQL Server context for an isolated EF in-memory store, and authenticates every request as an admin via a test scheme (no cookie login needed). Program.cs skips its startup migrate/seed/warmup under "Testing" so the in-memory provider can boot. - Tests cover the membership/payment endpoints end-to-end: recording a manual payment advances paid-through to ComputePaidThrough and stores a Paid payment row; member stats count paid vs unpaid by the renewal year (including the just-before-boundary case); the membership-renewal endpoint returns the compute-paid-through target. Adds Microsoft.AspNetCore.Mvc.Testing and EFCore.InMemory to the test project. Note: the .NET SDK isn't available in this environment, so these were written but not compiled/run here - CI is the first execution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FZmX5BKggFcrFTKk7BfEv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refactors the email composition logic into a reusable, host-agnostic component to support both the full-page email composer and a new in-modal payment reminder feature. Adds a membership payment reminder modal to the Members page that allows admins to send renewal emails to selected members.
Key Changes
New
emailComposer.tsxmodule: Extracts shared email composition logic including:HtmlEditorcomponent for contentEditable HTML editingComposeFormcomponent with subject/body fields and send state machineuseEmailTemplate()hook for loading and caching Resend templatessplitTemplate,trimFollowing,trimPreceding)isBodyEmpty,memberLabel,pluralize)New
PaymentReminderModal.tsx: Modal composer for membership renewal emails with:ComposeFormfor editing and sendingNew
paymentReminder.ts: Generates starter reminder email content with:paymentsEnabledflagUpdated
EmailCompose.tsx: Refactored to use extracted components:ComposeFormfor subject/body/send UIUpdated
Members.tsx: Adds payment reminder workflow:renewalTargetUtcto determine membership payment statusPaymentReminderModalwhen "Email selected" is clickedNew backend endpoint:
GetMembershipRenewalEndpointexposes the renewal target date (next 1 July boundary with late-join carry-over) so the admin UI can accurately determine which members need to renewUpdated
MemberDetail.tsx: Uses newfetchMembershipRenewalTarget()for consistent membership status determinationCSS updates: Adds checkbox column styling for admin tables
Implementation Details
ComposeFormcomponent is completely host-agnostic: it acceptsbuildRecipients()callback andonSent()handler, allowing different hosts (full-page composer vs. modal) to provide their own recipient logic and handle results differentlyhttps://claude.ai/code/session_012FZmX5BKggFcrFTKk7BfEv