diff --git a/app/components/standings/TeamScoreBreakdown.tsx b/app/components/standings/TeamScoreBreakdown.tsx index 8416ea4..8978135 100644 --- a/app/components/standings/TeamScoreBreakdown.tsx +++ b/app/components/standings/TeamScoreBreakdown.tsx @@ -1,4 +1,6 @@ +import { useState } from "react"; import { Link } from "react-router"; +import { ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react"; import { Card, CardContent } from "~/components/ui/card"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "~/components/ui/table"; import { Badge } from "~/components/ui/badge"; @@ -37,6 +39,35 @@ interface TeamScoreBreakdownProps { } | null; } +type SortColumn = "pick" | "points"; +type SortDirection = "asc" | "desc"; + +function SortIndicator({ column, sortColumn, sortDirection }: { column: SortColumn; sortColumn: SortColumn; sortDirection: SortDirection }) { + if (sortColumn !== column) return ; + return sortDirection === "asc" + ? + : ; +} + +function sortPicks( + picks: TeamScoreBreakdownProps["breakdown"]["picks"], + sortColumn: SortColumn, + sortDirection: SortDirection, +) { + const dir = sortDirection === "asc" ? 1 : -1; + return picks.slice().sort((a, b) => { + if (sortColumn === "pick") { + return (a.pickNumber - b.pickNumber) * dir; + } + // points: sort by actual first, then projected + const pointsDiff = a.points - b.points; + if (pointsDiff !== 0) return pointsDiff * dir; + const aProjected = a.projectedPoints ?? 0; + const bProjected = b.projectedPoints ?? 0; + return (aProjected - bProjected) * dir; + }); +} + /** * Display detailed team score breakdown with all drafted participants * Phase 4.3: Team breakdown pages @@ -48,6 +79,9 @@ export function TeamScoreBreakdown({ breakdown, standing, }: TeamScoreBreakdownProps) { + const [sortColumn, setSortColumn] = useState("pick"); + const [sortDirection, setSortDirection] = useState("asc"); + if (!breakdown.team) { return (
@@ -58,13 +92,16 @@ export function TeamScoreBreakdown({ const remaining = breakdown.totalCount - breakdown.completedCount; - // Flatten all picks sorted by sport name then pick number - const allPicks = breakdown.picks - .slice() - .toSorted((a, b) => { - const sportCmp = a.participant.sport.localeCompare(b.participant.sport); - return sportCmp !== 0 ? sportCmp : a.pickNumber - b.pickNumber; - }); + function handleSort(column: SortColumn) { + if (sortColumn === column) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + setSortColumn(column); + setSortDirection(column === "points" ? "desc" : "asc"); + } + } + + const allPicks = sortPicks(breakdown.picks, sortColumn, sortDirection); return (
@@ -104,13 +141,29 @@ export function TeamScoreBreakdown({ - Pick # + + + Sport Participant Position -
Points
-
actual / projected
+
diff --git a/app/components/standings/__tests__/TeamScoreBreakdown.test.tsx b/app/components/standings/__tests__/TeamScoreBreakdown.test.tsx index ff19451..63527e2 100644 --- a/app/components/standings/__tests__/TeamScoreBreakdown.test.tsx +++ b/app/components/standings/__tests__/TeamScoreBreakdown.test.tsx @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { BrowserRouter } from "react-router"; import { TeamScoreBreakdown } from "../TeamScoreBreakdown"; @@ -376,6 +377,130 @@ describe("TeamScoreBreakdown", () => { }); }); + describe("Sorting", () => { + function getParticipantNames() { + return screen + .getAllByRole("row") + .slice(1) // skip header row + .map((row) => row.querySelectorAll("td")[2]?.textContent ?? ""); + } + + it("should default sort by pick number ascending", () => { + renderWithRouter( + + ); + + const names = getParticipantNames(); + expect(names).toEqual(["Team A", "Driver B", "Team C"]); + }); + + it("should sort by pick number descending on second click", async () => { + const user = userEvent.setup(); + renderWithRouter( + + ); + + const pickHeader = screen.getByRole("button", { name: /pick #/i }); + await user.click(pickHeader); // toggle to desc + const names = getParticipantNames(); + expect(names).toEqual(["Team C", "Driver B", "Team A"]); + }); + + it("should sort by points descending on first click of Points header", async () => { + const user = userEvent.setup(); + renderWithRouter( + + ); + + // picks: Team A=100, Driver B=50, Team C=0 + const pointsHeader = screen.getByRole("button", { name: /points/i }); + await user.click(pointsHeader); + const names = getParticipantNames(); + expect(names).toEqual(["Team A", "Driver B", "Team C"]); + }); + + it("should sort by points ascending on second click of Points header", async () => { + const user = userEvent.setup(); + renderWithRouter( + + ); + + const pointsHeader = screen.getByRole("button", { name: /points/i }); + await user.click(pointsHeader); // desc + await user.click(pointsHeader); // asc + const names = getParticipantNames(); + expect(names).toEqual(["Team C", "Driver B", "Team A"]); + }); + + it("should use projected points as tiebreaker when actual points are equal", async () => { + const user = userEvent.setup(); + const tieBreakdown = { + ...mockBreakdown, + picks: [ + { + pickNumber: 1, + round: 1, + participant: { id: "p1", name: "Alpha", sport: "NFL", sportsSeasonId: "ss-nfl" }, + finalPosition: null, + points: 50, + projectedPoints: 10, + isComplete: false, + isPartialScore: false, + }, + { + pickNumber: 2, + round: 1, + participant: { id: "p2", name: "Beta", sport: "NFL", sportsSeasonId: "ss-nfl" }, + finalPosition: null, + points: 50, + projectedPoints: 30, + isComplete: false, + isPartialScore: false, + }, + ], + }; + + renderWithRouter( + + ); + + const pointsHeader = screen.getByRole("button", { name: /points/i }); + await user.click(pointsHeader); // sort by points desc → Beta (50/30) before Alpha (50/10) + const names = getParticipantNames(); + expect(names).toEqual(["Beta", "Alpha"]); + }); + }); + describe("Without Standing Data", () => { it("should render without standing prop", () => { renderWithRouter(