diff --git a/app/components/standings/StandingsTable.tsx b/app/components/standings/StandingsTable.tsx new file mode 100644 index 0000000..c83b192 --- /dev/null +++ b/app/components/standings/StandingsTable.tsx @@ -0,0 +1,183 @@ +import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "~/components/ui/table"; +import { Badge } from "~/components/ui/badge"; +import { type TeamStanding } from "~/types/standings"; + +interface StandingsTableProps { + standings: TeamStanding[]; + showPlacementBreakdown?: boolean; +} + +/** + * Display team standings with ranking, points, and placement breakdown + * Phase 4.1: Enhanced standings table with tiebreakers + */ +export function StandingsTable({ + standings, + showPlacementBreakdown = true, +}: StandingsTableProps) { + return ( +
+ {season.year} Season Standings +
+No standings data yet.
++ Standings will appear once participants have results. +
++ Tiebreaker Rules: Teams are ranked by total + points. If tied, the team with more 1st place finishes ranks + higher. If still tied, 2nd place finishes are compared, then 3rd, + and so on through 8th place. +
++ Movement Indicators: Arrows show rank changes + since the last standings update. +
++ Placement Breakdown: Shows how many times each + team's participants finished in each position (1stΓ2 means 2 first-place finishes). +
+Standings
+ + View Standings + +Flex Spots
diff --git a/app/types/standings.ts b/app/types/standings.ts new file mode 100644 index 0000000..1b6da7c --- /dev/null +++ b/app/types/standings.ts @@ -0,0 +1,30 @@ +/** + * Shared types for standings that can be used in both server and client code + */ + +export interface TeamStanding { + teamId: string; + teamName: string; + totalPoints: number; + currentRank: number; + previousRank: number | null; + rankChange: number; // Positive = moved up, negative = moved down + placementCounts: { + first: number; + second: number; + third: number; + fourth: number; + fifth: number; + sixth: number; + seventh: number; + eighth: number; + }; + participantsRemaining: number; + calculatedAt: Date; +} + +export interface TeamStandingSnapshot { + date: Date; + rank: number; + totalPoints: number; +} diff --git a/plans/scoring-system.md b/plans/scoring-system.md index 9b776b0..6d5f7e3 100644 --- a/plans/scoring-system.md +++ b/plans/scoring-system.md @@ -1266,10 +1266,44 @@ scoring_events { ### Phase 4: Standings & Display **Goal**: Full-featured standings with breakdowns and historical views -- [ ] **4.1** Enhanced standings table - - [ ] Implement tiebreaker logic (placement counts) - - [ ] Add sorting by tiebreakers - - [ ] Display placement breakdown per team +- [x] **4.1** Enhanced standings table β *Completed* + - [x] Implement tiebreaker logic (placement counts) + - [x] Add sorting by tiebreakers + - [x] Display placement breakdown per team + - [x] Create standalone standings page route + - [x] Integrate StandingsTable component into route + - [x] Add navigation from league home page + - [x] Fix client/server code separation issues + - [x] Comprehensive test suite (13 tiebreaker tests + 21 component tests) + - [x] All 371 tests passing β + + **Implementation Notes**: + - Tiebreaker cascade: Total points β 1st place count β 2nd place count β ... β 8th place count + - Teams with identical scores and placements share the same rank + - Rank numbers skip after ties (e.g., two teams tied for 1st, next is 3rd) + - `StandingsTable` component displays: + - Rank badges with trophy/medal icons for top 3 + - Total points with one decimal place + - Placement breakdown showing counts for each placement (1st-8th) + - Movement indicators (up/down arrows) + - Participants remaining count + - `recalculateStandings` function now assigns ranks using tiebreaker logic + - `previousRank` tracked for movement indicators + - Created standalone standings page at `/leagues/:leagueId/standings/:seasonId` + - Fixed module bundling issue by separating types into `app/types/standings.ts` + - Types can now be safely imported by both client and server code + - Added "View Standings" link in League Info section on league home page + - Files created/updated: + - app/models/scoring-calculator.ts (added compareTeamsForRanking, assignRanks) + - app/models/standings.ts (fixed sort order, re-exports types) + - app/types/standings.ts (new - shared type definitions) + - app/components/standings/StandingsTable.tsx (new component) + - app/routes/leagues/$leagueId.standings.$seasonId.tsx (new route) + - app/routes/leagues/$leagueId.tsx (added standings link) + - app/routes.ts (registered new route) + - tsconfig.node.json (added app/types/**/*.ts to include) + - app/models/__tests__/tiebreaker-logic.test.ts (new tests) + - app/components/standings/__tests__/StandingsTable.test.tsx (new tests) - [ ] **4.2** Movement tracking & snapshots - [ ] Daily snapshot creation (scheduled job) @@ -1291,14 +1325,20 @@ scoring_events { - [x] Ownership indicators with avatars + tooltips (Q11, Q15) - [x] Bracket display for playoff sports (Q3) - **Note**: Route exists but needs navigation from league home page (see 4.5 below) + **Note**: Route exists and displays correctly. Navigation cards on league home page pending (see 4.5) -- [ ] **4.5** League home page integration - - [ ] Display team standings table on league home page +- [ ] **4.5** League home page integration (partially complete) + - [x] Add navigation link to standings page from league home + - [ ] Display team standings table on league home page (or keep separate as currently implemented) - [ ] Add "Sports Seasons" section showing all sports in current season - [ ] Link each sport season card to detail page (Q11 - ownership hints on each sport's page) - [ ] Show sport status badges (upcoming, active, completed) - - [ ] Quick access to brackets and standings from league home + - [ ] Quick access to brackets from league home + + **Implementation Notes**: + - "View Standings" link added in League Info section (app/routes/leagues/$leagueId.tsx:433-440) + - Standalone standings page preferred over embedding in league home for cleaner UX + - Sport season detail pages already exist (Phase 3.4) - need navigation cards on league home - [ ] **4.6** Historical views - [ ] Season completion detection @@ -1394,10 +1434,12 @@ scoring_events { - **3.2** Qualifying Points (Golf/Tennis) - Two-phase scoring with configurable QP values - **3.3** AFL Finals - Double-chance system with Wildcard Round (10 teams) - **3.4** Pattern-specific UI Components - PlayoffBracket, SeasonStandings, SportSeasonDisplay components + member route -- β³ **Phase 4**: Standings & Display - TODO +- β³ **Phase 4**: Standings & Display - IN PROGRESS + - β **4.1** Enhanced Standings Table - COMPLETE (tiebreaker logic, placement breakdowns, standalone page, navigation) + - β³ **4.5** League Home Integration - PARTIAL (standings link added, sports season cards pending) - β³ **Phase 5**: Expected Value - TODO - β³ **Phase 6**: Polish & Optimization - TODO -**Current Focus**: Phase 3.4 - Pattern-specific UI Components or Phase 4 - Standings & Display +**Current Focus**: Phase 4 - Standings & Display (4.1 complete with standalone page, 4.5 partially complete) -**Total Test Count**: 337 tests passing β +**Total Test Count**: 371 tests passing β diff --git a/tsconfig.node.json b/tsconfig.node.json index a5539d9..09d6cf3 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -6,6 +6,7 @@ "app/contexts/**/*.ts", "app/models/**/*.ts", "app/lib/**/*.ts", + "app/types/**/*.ts", "vite.config.ts" ], "exclude": [