Show events on multiple dates when games span requested date range (#254)

* Fix admin Games to Score panel missing bracket events

The panel was silently dropping bracket events in two cases:

1. When a bracket event had `scoringEvents.eventDate` set to a future
   round date (e.g. "April 5") but an individual game's `scheduledAt`
   was today, the old dedup code picked `eventDate` first and the
   component filter `displayDate === today` never matched.

2. When an event had games on both today and tomorrow, whichever row
   the unordered step-1 query returned first "won" — potentially hiding
   a today game from the today tab entirely.

Fix: replace the `Map<id, firstDate>` deduplication with
`Map<id, Set<date>>` that collects every requested date an event
qualifies for (via either `eventDate` or `gameDate`). The return uses
`flatMap` to emit one entry per (event, date) pair, so events with
games on multiple days now appear correctly in both tabs.

https://claude.ai/code/session_01QkFxCF4xmKjdVCNPysWDML

* Fix lint errors: replace .sort() with .toSorted() and remove non-null assertion

- Replace Array#sort() with Array#toSorted() in two places (unicorn rule)
- Remove non-null assertion on Map#get(); use early continue instead

https://claude.ai/code/session_01QkFxCF4xmKjdVCNPysWDML

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-04-01 13:39:03 -07:00 committed by GitHub
parent 6df66e9cc6
commit c5ccee2225
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 97 additions and 19 deletions

View file

@ -270,4 +270,55 @@ describe("getEventsForDates", () => {
expect(result[0].displayDate).toBe("2026-03-20");
});
it("uses gameDate over eventDate when eventDate is outside requested dates", async () => {
// Bracket event: eventDate is the round's overall date (not in range),
// but an individual game is scheduled on the requested date.
mockSelect.mockReturnValue(
makeSelectChain([{ id: "e1", eventDate: "2026-04-05", gameDate: "2026-03-20" }])
);
mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: "2026-04-05" })]);
const result = await getEventsForDates(["2026-03-20"]);
expect(result).toHaveLength(1);
expect(result[0].displayDate).toBe("2026-03-20");
});
it("returns two entries for an event with games on both requested dates", async () => {
// Same event has a game today AND a game tomorrow — should appear in both tabs.
mockSelect.mockReturnValue(
makeSelectChain([
{ id: "e1", eventDate: null, gameDate: "2026-03-20" },
{ id: "e1", eventDate: null, gameDate: "2026-03-21" },
])
);
mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: null })]);
const result = await getEventsForDates(["2026-03-20", "2026-03-21"]);
expect(result).toHaveLength(2);
const displayDates = result.map((e) => e.displayDate).toSorted();
expect(displayDates).toEqual(["2026-03-20", "2026-03-21"]);
// Both entries share the same event id
expect(result[0].id).toBe("e1");
expect(result[1].id).toBe("e1");
});
it("deduplicates when multiple matches in same bracket have games on the same day", async () => {
// Two rows for the same event on the same gameDate (e.g. two matches in today's bracket)
mockSelect.mockReturnValue(
makeSelectChain([
{ id: "e1", eventDate: null, gameDate: "2026-03-20" },
{ id: "e1", eventDate: null, gameDate: "2026-03-20" },
])
);
mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: null })]);
const result = await getEventsForDates(["2026-03-20"]);
// Should appear only once, not twice
expect(result).toHaveLength(1);
expect(result[0].displayDate).toBe("2026-03-20");
});
});

View file

@ -665,14 +665,33 @@ export async function getEventsForDates(
if (rows.length === 0) return [];
// Deduplicate: keep first matched row per event to derive displayDate.
const displayDateMap = new Map<string, string | null>();
// Collect ALL dates each event qualifies for. For bracket events, the
// game's scheduledAt date (gameDate) takes priority over the event-level
// eventDate — the event-level date might be set to the overall round date
// (e.g. "April 5") while an individual game is actually scheduled today.
// Using a Set per event also de-duplicates naturally when multiple matches
// in the same bracket have games on the same day.
const eventDatesMap = new Map<string, Set<string>>();
for (const row of rows) {
if (!displayDateMap.has(row.id)) {
displayDateMap.set(row.id, row.eventDate ?? row.gameDate ?? null);
if (!eventDatesMap.has(row.id)) {
eventDatesMap.set(row.id, new Set());
}
const dateSet = eventDatesMap.get(row.id);
if (!dateSet) continue;
if (row.eventDate && dates.includes(row.eventDate)) {
dateSet.add(row.eventDate);
}
if (row.gameDate && dates.includes(row.gameDate)) {
dateSet.add(row.gameDate);
}
// Fallback: row passed the WHERE clause so at least one date is relevant;
// if neither mapped to a requested date (shouldn't happen), keep something.
if (dateSet.size === 0) {
const fallback = row.gameDate ?? row.eventDate ?? null;
if (fallback) dateSet.add(fallback);
}
}
const eventIds = [...displayDateMap.keys()];
const eventIds = [...eventDatesMap.keys()];
// Step 2: fetch the matched events with sport season + sport info.
const events = await db.query.scoringEvents.findMany({
@ -691,20 +710,28 @@ export async function getEventsForDates(
},
});
return events.map((e) => ({
id: e.id,
name: e.name,
eventDate: e.eventDate,
displayDate: displayDateMap.get(e.id) ?? e.eventDate ?? null,
eventStartsAt: e.eventStartsAt,
eventType: e.eventType,
isComplete: e.isComplete,
completedAt: e.completedAt,
sportsSeasonId: e.sportsSeasonId,
sportsSeasonName: e.sportsSeason.name,
sportName: e.sportsSeason.sport.name,
sportSlug: e.sportsSeason.sport.slug,
}));
// Return one entry per (event, date) pair so that an event with games on
// both today and tomorrow appears in both tabs of the dashboard widget.
return events.flatMap((e) => {
const matchingDates = [...(eventDatesMap.get(e.id) ?? [])].toSorted();
const base = {
id: e.id,
name: e.name,
eventDate: e.eventDate,
eventStartsAt: e.eventStartsAt,
eventType: e.eventType,
isComplete: e.isComplete,
completedAt: e.completedAt,
sportsSeasonId: e.sportsSeasonId,
sportsSeasonName: e.sportsSeason.name,
sportName: e.sportsSeason.sport.name,
sportSlug: e.sportsSeason.sport.slug,
};
if (matchingDates.length === 0) {
return [{ ...base, displayDate: e.eventDate ?? null }];
}
return matchingDates.map((date) => ({ ...base, displayDate: date }));
});
}
/**