From 906fb6865ab2639c664ad53c12e8ba0473740da8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 01:49:53 +0000 Subject: [PATCH] Add sortable columns to draft picks table on manager page Pick # column sorts by pick number (asc by default), Points column sorts by actual points then projected as tiebreaker (desc by default). Clicking a column header toggles sort direction with visual indicators. https://claude.ai/code/session_01XBnm7eKxerR7WjwrqqJPwe --- .../standings/TeamScoreBreakdown.tsx | 62 +++++++-- .../__tests__/TeamScoreBreakdown.test.tsx | 125 ++++++++++++++++++ 2 files changed, 177 insertions(+), 10 deletions(-) diff --git a/app/components/standings/TeamScoreBreakdown.tsx b/app/components/standings/TeamScoreBreakdown.tsx index 8416ea4..7833222 100644 --- a/app/components/standings/TeamScoreBreakdown.tsx +++ b/app/components/standings/TeamScoreBreakdown.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Link } from "react-router"; import { Card, CardContent } from "~/components/ui/card"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "~/components/ui/table"; @@ -37,6 +38,9 @@ interface TeamScoreBreakdownProps { } | null; } +type SortColumn = "pick" | "points"; +type SortDirection = "asc" | "desc"; + /** * Display detailed team score breakdown with all drafted participants * Phase 4.3: Team breakdown pages @@ -48,6 +52,9 @@ export function TeamScoreBreakdown({ breakdown, standing, }: TeamScoreBreakdownProps) { + const [sortColumn, setSortColumn] = useState("pick"); + const [sortDirection, setSortDirection] = useState("asc"); + if (!breakdown.team) { return (
@@ -58,13 +65,32 @@ 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 = breakdown.picks.slice().sort((a, b) => { + const dir = sortDirection === "asc" ? 1 : -1; + 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; + }); + + function SortIndicator({ column }: { column: SortColumn }) { + if (sortColumn !== column) return ; + return {sortDirection === "asc" ? "↑" : "↓"}; + } return (
@@ -104,13 +130,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(