diff --git a/app/models/__tests__/upcoming-calendar.test.ts b/app/models/__tests__/upcoming-calendar.test.ts index ffa5587..b4cc94d 100644 --- a/app/models/__tests__/upcoming-calendar.test.ts +++ b/app/models/__tests__/upcoming-calendar.test.ts @@ -459,6 +459,83 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { expect(result[0].matchLabel).toBeNull(); }); + it("includes the P1 vs P2 matchup in matchLabel alongside the round name", async () => { + const rows = [{ + id: "e1", name: "IEM Cologne 2026", eventDate: "2026-06-19", + eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", + participant1Id: "team-spirit", participant2Id: "g2-esports", + round: "Quarterfinals", gameNumber: 1, scheduledAt: null, + }]; + mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); + mockDb.query.seasonParticipants.findMany.mockResolvedValueOnce([ + { id: "team-spirit", name: "Team Spirit" }, + { id: "g2-esports", name: "G2 Esports" }, + ]); + + const result = await getUpcomingEventsForDraftedParticipants( + SPORTS_SEASON_ID, "playoff_bracket", + [makeParticipant("g2-esports", "G2 Esports")], + DATE_FROM, DATE_TO + ); + + expect(result[0].matchLabel).toBe("Quarterfinals — Team Spirit vs G2 Esports"); + // relevantParticipants stays drafted-only — the opponent is only surfaced via matchLabel. + expect(result[0].relevantParticipants).toEqual([{ id: "g2-esports", name: "G2 Esports" }]); + }); + + it("shows just the matchup (no round prefix) when round name is redundant with event name", async () => { + const rows = [{ + id: "e1", name: "Knockout Stage", eventDate: null, + eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", + participant1Id: "barcelona", participant2Id: "sporting", + round: "Knockout Stage", gameNumber: 1, scheduledAt: null, + }]; + mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); + mockDb.query.seasonParticipants.findMany.mockResolvedValueOnce([ + { id: "barcelona", name: "Barcelona" }, + { id: "sporting", name: "Sporting" }, + ]); + + const result = await getUpcomingEventsForDraftedParticipants( + SPORTS_SEASON_ID, "playoff_bracket", + [makeParticipant("barcelona", "Barcelona")], + DATE_FROM, DATE_TO + ); + + expect(result[0].matchLabel).toBe("Barcelona vs Sporting"); + }); + + it("includes the matchup in matchLabel for series games", async () => { + const gameTime1 = new Date("2025-04-09T13:00:00.000Z"); + const rows = [{ + id: "e1", name: "UCL QF", eventDate: null, + eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", + participant1Id: "barcelona", participant2Id: "sporting", + round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime1, + }]; + mockDb.selectDistinct + .mockReturnValueOnce(makeSelectChain(rows)) + .mockReturnValueOnce(makeMaxGameChain([ + { eventId: "e1", gameNumber: 1 }, + { eventId: "e1", gameNumber: 2 }, + ])); + mockDb.query.seasonParticipants.findMany.mockResolvedValueOnce([ + { id: "barcelona", name: "Barcelona" }, + { id: "sporting", name: "Sporting" }, + ]); + + const result = await getUpcomingEventsForDraftedParticipants( + SPORTS_SEASON_ID, "playoff_bracket", + [makeParticipant("barcelona", "Barcelona")], + DATE_FROM, DATE_TO + ); + + expect(result[0].matchLabel).toBe("Knockout Stage Game #1 — Barcelona vs Sporting"); + }); + it("shows game number when only game #2 is in the window (game #1 already played)", async () => { const rows = [{ id: "e1", name: "PDC World Championship", eventDate: "2025-04-10", diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index 4752663..836a595 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -607,6 +607,24 @@ export async function getUpcomingEventsForDraftedParticipants( ) .orderBy(asc(schema.scoringEvents.eventDate)); + // Look up names for ALL participants in these matches (not just drafted + // ones) so matchLabel can show the full "P1 vs P2" matchup, mirroring the + // group-stage matchLabel below. + const allMatchParticipantIds = [ + ...new Set( + rows.flatMap((r) => [r.participant1Id, r.participant2Id]).filter((id): id is string => !!id) + ), + ]; + const matchParticipantNameById = new Map(); + if (allMatchParticipantIds.length > 0) { + const matchParticipantRows = await db.query.seasonParticipants.findMany({ + where: inArray(schema.seasonParticipants.id, allMatchParticipantIds), + }); + for (const p of matchParticipantRows) { + matchParticipantNameById.set(p.id, p.name); + } + } + // Determine max game number per event across ALL games (not date-filtered) // so we can distinguish single-game matchups from multi-game series. const eventIdsFromRows = [...new Set(rows.map((r) => r.id))]; @@ -644,6 +662,7 @@ export async function getUpcomingEventsForDraftedParticipants( const gameKeysByEntry = new Map>(); const earliestTimeByEntry = new Map(); const isSeriesByEntry = new Map(); + const matchupIdsByEntry = new Map(); for (const row of rows) { const isSeries = (maxGameNumberByEvent.get(row.id) ?? 1) > 1; @@ -667,6 +686,7 @@ export async function getUpcomingEventsForDraftedParticipants( participantsByEntry.set(entryKey, new Map()); gameKeysByEntry.set(entryKey, new Set()); isSeriesByEntry.set(entryKey, isSeries); + matchupIdsByEntry.set(entryKey, { p1Id: row.participant1Id, p2Id: row.participant2Id }); } const pMap = participantsByEntry.get(entryKey); @@ -702,25 +722,35 @@ export async function getUpcomingEventsForDraftedParticipants( const isSeries = isSeriesByEntry.get(entryKey) ?? false; const gameKeys = gameKeysByEntry.get(entryKey) ?? new Set(); + const matchupIds = matchupIdsByEntry.get(entryKey); + const p1Name = matchupIds?.p1Id ? matchParticipantNameById.get(matchupIds.p1Id) : undefined; + const p2Name = matchupIds?.p2Id ? matchParticipantNameById.get(matchupIds.p2Id) : undefined; + const matchupLabel = p1Name && p2Name ? `${p1Name} vs ${p2Name}` : null; + + let roundLabel: string | null = null; if (isSeries) { if (gameKeys.size >= 1) { const [round, gameNumberStr] = [...gameKeys][0].split("|"); - event.matchLabel = + roundLabel = round === event.name ? `Game #${gameNumberStr}` : `${round} Game #${gameNumberStr}`; } } else { // Single-game: omit the game number, just show the round name. if (gameKeys.size === 1) { const [round] = [...gameKeys][0].split("|"); - event.matchLabel = round === event.name ? null : round; + roundLabel = round === event.name ? null : round; } else if (gameKeys.size > 1) { const rounds = new Set([...gameKeys].map((k) => k.split("|")[0])); if (rounds.size === 1) { const round = [...rounds][0]; - event.matchLabel = round === event.name ? null : round; + roundLabel = round === event.name ? null : round; } } } + + event.matchLabel = roundLabel && matchupLabel + ? `${roundLabel} — ${matchupLabel}` + : roundLabel ?? matchupLabel; } const bracketResults = Array.from(entryMap.values());