From f8bb60b97c351765330ef37054f65c18bff9027f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 02:15:01 +0000 Subject: [PATCH 1/2] feat(broadcasts): filter recipients by missing submissions in a division Organizers running online competitions can now target athletes who haven't submitted scores for every event required of a division, without bothering those who have already submitted everything. Honors event-division mappings when scoping required events. Pending-invite teammates are always kept since they have no user account to check submissions against. Applied after question filters so team-inheritance in applyAthleteQuestionFilters still has the captain row available to resolve teammate matches. Recipient-level dedup already handles athletes registered in multiple divisions (same userId collapses to one row). --- .../organizer/$competitionId/broadcasts.tsx | 28 ++- .../src/server-fns/broadcast-fns.ts | 175 +++++++++++++++++- .../test/server-fns/broadcast-fns.test.ts | 22 +++ lat.md/organizer-dashboard.md | 12 ++ 4 files changed, 227 insertions(+), 10 deletions(-) diff --git a/apps/wodsmith-start/src/routes/compete/organizer/$competitionId/broadcasts.tsx b/apps/wodsmith-start/src/routes/compete/organizer/$competitionId/broadcasts.tsx index b0e39548e..0b65e1f10 100644 --- a/apps/wodsmith-start/src/routes/compete/organizer/$competitionId/broadcasts.tsx +++ b/apps/wodsmith-start/src/routes/compete/organizer/$competitionId/broadcasts.tsx @@ -190,6 +190,7 @@ type AudienceFilterType = | "volunteers" | "volunteer_role" | "pending_teammates" + | "missing_submissions" const VOLUNTEER_ROLES = [ { value: "judge", label: "Judge" }, @@ -236,7 +237,10 @@ function ComposeCard({ // Determine which questions to show based on audience type const relevantQuestions = useMemo(() => { - const isAthleteAudience = filterType === "all" || filterType === "division" + const isAthleteAudience = + filterType === "all" || + filterType === "division" || + filterType === "missing_submissions" const isVolunteerAudience = filterType === "volunteers" || filterType === "volunteer_role" const isPublic = filterType === "public" @@ -261,9 +265,11 @@ function ComposeCard({ divisionId, } : { type: "pending_teammates" as const } - : { - type: filterType as "all" | "public" | "volunteers", - } + : filterType === "missing_submissions" && divisionId + ? { type: "missing_submissions" as const, divisionId } + : { + type: filterType as "all" | "public" | "volunteers", + } if (questionFilters.length > 0) { return { ...base, questionFilters } @@ -274,7 +280,8 @@ function ComposeCard({ // Auto-fetch recipient count when filter is complete const filterReady = (filterType !== "division" || !!divisionId) && - (filterType !== "volunteer_role" || !!volunteerRole) + (filterType !== "volunteer_role" || !!volunteerRole) && + (filterType !== "missing_submissions" || !!divisionId) // Debounce the preview call const debounceRef = useRef | null>(null) @@ -340,6 +347,11 @@ function ComposeCard({ return } + if (filterType === "missing_submissions" && !divisionId) { + toast.error("Please select a division") + return + } + if (filterType === "volunteer_role" && !volunteerRole) { toast.error("Please select a volunteer role") return @@ -421,6 +433,9 @@ function ComposeCard({ Everyone (Public) All Athletes Athletes by Division + + Athletes Missing Submissions (by Division) + All Volunteers Volunteers by Role @@ -432,7 +447,8 @@ function ComposeCard({ {(filterType === "division" || - filterType === "pending_teammates") && ( + filterType === "pending_teammates" || + filterType === "missing_submissions") && (