Skip to content

[EPIC-003] Story 7: Statistics & Analytics#54

Merged
TheEagleByte merged 7 commits intomainfrom
issue-23-statistics-analytics
Oct 1, 2025
Merged

[EPIC-003] Story 7: Statistics & Analytics#54
TheEagleByte merged 7 commits intomainfrom
issue-23-statistics-analytics

Conversation

@TheEagleByte
Copy link
Copy Markdown
Owner

@TheEagleByte TheEagleByte commented Oct 1, 2025

Summary

Implements comprehensive statistics and analytics for Planning Poker sessions (Issue #23).

Features Implemented

✅ Calculate Average/Median Estimates

  • Backend calculation functions for numeric vote statistics
  • Display in vote results with consensus percentage

✅ Show Vote Distribution Chart

  • Enhanced existing distribution chart in VoteResults component
  • Added consensus percentage metric (≥70% highlighted in green)
  • Visual indicators for high consensus

✅ Track Estimation Velocity

  • Automatic velocity calculation: stories per hour
  • Average and median time per story
  • Uses vote timestamps and story completion times

✅ Display Consensus Percentage

  • Calculates % of votes within 1 card value of mode
  • Displayed prominently in vote results statistics
  • Color-coded for quick identification

✅ Create Session Summary

  • Comprehensive SessionSummary component showing:
    • Stories completed (estimated/pending/skipped)
    • Average/median estimation time
    • Overall consensus rate
    • Estimation velocity (stories per hour)
    • Participant contribution breakdown
    • Estimate distribution visualization

✅ Export Results to CSV

  • ExportButton component with loading states
  • CSV format includes:
    • Story details (title, final estimate, votes)
    • Vote distribution and consensus metrics
    • Time taken per story
    • Participant votes breakdown
    • Session summary statistics

Technical Implementation

Backend Infrastructure

  • src/lib/poker/statistics.ts: Core calculation functions
  • src/lib/poker/csv-export.ts: CSV generation utilities
  • src/lib/poker/actions.ts: New getSessionStatistics server action
  • src/lib/poker/types.ts: New types for SessionStatistics, StoryStatistics, ParticipantStatistics, ExportData

Frontend Components

  • src/components/poker/SessionSummary.tsx: Statistics dashboard
  • src/components/poker/ExportButton.tsx: CSV export functionality
  • src/components/poker/VoteResults.tsx: Enhanced with consensus %
  • src/hooks/use-poker-statistics.ts: React Query hook for stats

Integration

  • Added SessionSummary to session page after Session Settings
  • Added ExportButton to session header
  • Real-time updates as stories are estimated

Testing

  • ✅ Build passes with no TypeScript errors
  • ✅ Linting passes (no new warnings)
  • ✅ CodeRabbit review completed

Documentation

  • Updated CHANGELOG.md with comprehensive feature list
  • Added JSDoc comments to all new functions

Screenshots/Demo

Statistics will be visible in session page once stories are estimated. Export button disabled until data is available.

Closes

Closes #23


🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a Planning Poker analytics dashboard with session KPIs, participant contributions, distribution visuals, and session summaries.
    • Added CSV export for session analytics with export/loading states and real-time export readiness.
    • Export button and session summary now accessible from the session view.
  • Improvements

    • Dynamic consensus and velocity metrics with high-consensus indicators.
    • Responsive layout and visual refinements for clearer analytics.

- Add types for SessionStatistics, StoryStatistics, ParticipantStatistics, ExportData
- Create statistics.ts with calculation functions:
  - calculateConsensusPercentage: % votes within 1 step of mode
  - calculateStoryVelocity: time from first vote to final estimate
  - calculateSessionStatistics: aggregate session-wide metrics
- Add getSessionStatistics server action to fetch and compute statistics
- Create csv-export.ts utility for exporting session data to CSV

Relates to #23
- Import calculateConsensusPercentage from statistics utility
- Add consensusPercentage to VoteAnalysis interface
- Calculate consensus % (votes within 1 step of mode)
- Display consensus % in statistics summary with 5-column grid
- Highlight consensus >=70% with green background
- Show percentage with 0 decimal places

Relates to #23
- Create use-poker-statistics hook with React Query integration
- Create ExportButton component:
  - Export session statistics to CSV
  - Disable when no estimated stories
  - Loading state with spinner
  - Toast notifications
- Create SessionSummary component:
  - Display key metrics: stories completed, avg time, velocity, consensus rate
  - Show participant contributions with vote counts
  - Display estimate distribution with bar charts
  - Responsive grid layout
  - Loading and empty states

Relates to #23
- Add SessionSummary component after Session Settings card
- Add ExportButton to session header area
- Import required components
- Layout adjustments for better UX

Relates to #23
Add comprehensive documentation for Issue #23 including:
- Session statistics dashboard
- Consensus percentage tracking
- Estimation velocity metrics
- CSV export functionality
- Participant analytics

Relates to #23
- Add null coalescing for session.description in ExportButton
- Add type assertion for estimation_sequence in getSessionStatistics
- Add type assertion for custom_sequence JSON from database
- Add null coalescing for story.final_estimate in statistics
- Import EstimationSequenceType for type safety

Relates to #23
Copilot AI review requested due to automatic review settings October 1, 2025 16:44
@vercel
Copy link
Copy Markdown

vercel bot commented Oct 1, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
scrumkit Ready Ready Preview Comment Oct 1, 2025 4:57pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds Planning Poker analytics: types and calculation utilities, a server action to compute per-session statistics, a react-query hook, UI components (SessionSummary, ExportButton), CSV export utilities, VoteResults consensus display, session page integration, and CHANGELOG entry.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Adds "Planning Poker Statistics & Analytics" entry under Unreleased.
Page integration
src/app/poker/[sessionUrl]/page.tsx
Renders session.description, adds ExportButton, reworks header layout, and inserts SessionSummary below session info.
UI components
src/components/poker/SessionSummary.tsx, src/components/poker/ExportButton.tsx, src/components/poker/VoteResults.tsx
New SessionSummary dashboard using session statistics; new ExportButton to prepare/export CSV with loading/toasts; VoteResults extended to compute and display consensusPercentage and updated analysis layout.
Hook: statistics fetching
src/hooks/use-poker-statistics.ts
Adds pokerStatisticsKeys and useSessionStatistics (react-query) with 5‑minute cache.
Server action
src/lib/poker/actions.ts
Adds getSessionStatistics(sessionId) to load session, stories, and votes, then compute session analytics via the statistics utilities.
Statistics core
src/lib/poker/statistics.ts, src/lib/poker/types.ts
New analytics functions (calculateConsensusPercentage, calculateStoryVelocity, calculateStoryStatistics, calculateSessionStatistics) and new types (StoryStatistics, SessionStatistics, ParticipantStatistics, ExportData).
CSV export
src/lib/poker/csv-export.ts
Adds prepareExportData, exportSessionToCSV, and downloadCSV to build CSV content and trigger browser download.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant P as Session Page
  participant SS as SessionSummary
  participant H as useSessionStatistics
  participant Q as react-query
  participant A as getSessionStatistics
  participant S as statistics.ts
  participant DB as Data Store

  U->>P: Open session page
  P->>SS: Render with sessionId
  SS->>H: useSessionStatistics(sessionId)
  H->>Q: query(key, fetcher)
  Q->>A: fetch statistics(sessionId)
  A->>DB: load session, stories, votes
  A->>S: calculateSessionStatistics(...)
  S-->>A: SessionStatistics
  A-->>Q: return SessionStatistics
  Q-->>H: cache + return
  H-->>SS: data/loading/error
  SS-->>U: Render dashboard
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant EB as ExportButton
  participant H as useSessionStatistics
  participant E as csv-export.ts
  participant B as Browser

  U->>EB: Click "Export CSV"
  EB->>H: read cached statistics
  EB->>E: prepareExportData(session, statistics)
  E-->>EB: ExportData
  EB->>E: exportSessionToCSV(ExportData)
  E-->>EB: csvContent
  EB->>E: downloadCSV(csvContent, filename)
  E->>B: trigger download
  B-->>U: CSV file downloaded
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I hop through cards and datasets bright,
Counting votes by moonlit night.
Consensus hums, a tidy tree,
CSV wings flutter, downloaded free.
Stories tallied — hop, hop, hooray! 🐇📊

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “[EPIC-003] Story 7: Statistics & Analytics” clearly references the epic and story number and concisely summarizes the primary change—adding statistics and analytics features—without extraneous details, making it easy for reviewers to understand the focus of the pull request at a glance.
Linked Issues Check ✅ Passed The changes fully implement each coding-related task from issue #23 by adding backend functions to calculate average and median estimates and consensus percentage, enhancing VoteResults with a distribution chart and consensus indicator, calculating and displaying estimation velocity in SessionSummary, creating a comprehensive SessionSummary component, and providing an ExportButton for CSV exports, thereby satisfying all acceptance criteria.
Out of Scope Changes Check ✅ Passed All modifications are directly tied to the analytics and export functionality defined in the linked issue, with no unrelated files or features altered, ensuring that no out-of-scope changes have been introduced.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-23-statistics-analytics

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements comprehensive statistics and analytics for Planning Poker sessions, adding detailed metrics, visualization, and export capabilities to help teams analyze their estimation performance.

  • Comprehensive statistics dashboard with session-wide analytics
  • CSV export functionality for detailed session data
  • Real-time consensus tracking and velocity metrics

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/lib/poker/types.ts Adds new TypeScript interfaces for statistics data structures
src/lib/poker/statistics.ts Core calculation functions for consensus, velocity, and session analytics
src/lib/poker/csv-export.ts CSV generation and download utilities
src/lib/poker/actions.ts Server action to fetch comprehensive session statistics
src/hooks/use-poker-statistics.ts React Query hook for statistics data fetching
src/components/poker/VoteResults.tsx Enhanced vote results with consensus percentage display
src/components/poker/SessionSummary.tsx New statistics dashboard component
src/components/poker/ExportButton.tsx CSV export functionality with loading states
src/app/poker/[sessionUrl]/page.tsx Integration of statistics components into session page
CHANGELOG.md Documentation of new features

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


exportData.stories.forEach((story) => {
const row = [
escapeCsvField(story.title),
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSV header includes 'Description' but the data row omits the description field. Either add the description field to the data row or remove it from the header.

Suggested change
escapeCsvField(story.title),
escapeCsvField(story.title),
escapeCsvField(story.description || ""),

Copilot uses AI. Check for mistakes.
.slice(0, 5); // Top 5

return {
sessionId: stories[0]?.session_id || "",
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no stories exist, accessing stories[0] returns undefined, making sessionId an empty string. The sessionId parameter should be used instead to ensure correct session identification.

Suggested change
sessionId: stories[0]?.session_id || "",
sessionId: sessionId,

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/poker/VoteResults.tsx (1)

200-246: Keep the consensus summary visible for all decks.

Because the entire stats grid is guarded by analysis.numericVotes.length > 0, any non-numeric deck (e.g., T-shirt sizes where parseFloat returns NaN) hides the consensus card altogether—even though votes exist and we still need to highlight high consensus. That regresses the new consensus feature for those sessions. Render the grid unconditionally and guard only the numeric-only cards (average/median/range) so consensus is always shown.

-          {analysis.numericVotes.length > 0 && (
-            <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
+          <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
             <div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
               <p className="text-xs text-slate-600 dark:text-slate-400">Most Common</p>
               <p className="text-lg font-bold text-slate-900 dark:text-slate-100">
                 {analysis.mode.join(", ")}
               </p>
             </div>
             {analysis.average !== undefined && (
               <div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
                 <p className="text-xs text-slate-600 dark:text-slate-400">Average</p>
                 <p className="text-lg font-bold text-slate-900 dark:text-slate-100">
                   {analysis.average.toFixed(1)}
                 </p>
               </div>
             )}
             {analysis.median !== undefined && (
               <div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
                 <p className="text-xs text-slate-600 dark:text-slate-400">Median</p>
                 <p className="text-lg font-bold text-slate-900 dark:text-slate-100">
                   {analysis.median}
                 </p>
               </div>
             )}
-            <div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
-              <p className="text-xs text-slate-600 dark:text-slate-400">Range</p>
-              <p className="text-lg font-bold text-slate-900 dark:text-slate-100">
-                {Math.min(...analysis.numericVotes)} - {Math.max(...analysis.numericVotes)}
-              </p>
-            </div>
+            {analysis.numericVotes.length > 0 && (
+              <div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
+                <p className="text-xs text-slate-600 dark:text-slate-400">Range</p>
+                <p className="text-lg font-bold text-slate-900 dark:text-slate-100">
+                  {Math.min(...analysis.numericVotes)} - {Math.max(...analysis.numericVotes)}
+                </p>
+              </div>
+            )}
             <div className={cn(
               "text-center p-3 rounded-lg",
               analysis.consensusPercentage >= 70
                 ? "bg-green-50 dark:bg-green-900/20"
                 : "bg-slate-50 dark:bg-slate-800"
             )}>
               <p className="text-xs text-slate-600 dark:text-slate-400">Consensus</p>
               <p className={cn(
                 "text-lg font-bold",
                 analysis.consensusPercentage >= 70
                   ? "text-green-700 dark:text-green-400"
                   : "text-slate-900 dark:text-slate-100"
               )}>
                 {analysis.consensusPercentage.toFixed(0)}%
               </p>
             </div>
-            </div>
-          )}
+          </div>
🧹 Nitpick comments (1)
src/components/poker/SessionSummary.tsx (1)

202-225: Avoid mutating cached statistics in render.

Calling .sort directly on statistics.participantStats mutates the React Query cache in place, which can leak mutations across subscribers and future re-renders. Clone before sorting so the cached data stays immutable.

-              {statistics.participantStats
-                .sort((a, b) => b.totalVotes - a.totalVotes)
+              {[...statistics.participantStats]
+                .sort((a, b) => b.totalVotes - a.totalVotes)
                 .map((participant) => (
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f377fa and ffce434.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • src/app/poker/[sessionUrl]/page.tsx (3 hunks)
  • src/components/poker/ExportButton.tsx (1 hunks)
  • src/components/poker/SessionSummary.tsx (1 hunks)
  • src/components/poker/VoteResults.tsx (5 hunks)
  • src/hooks/use-poker-statistics.ts (1 hunks)
  • src/lib/poker/actions.ts (2 hunks)
  • src/lib/poker/csv-export.ts (1 hunks)
  • src/lib/poker/statistics.ts (1 hunks)
  • src/lib/poker/types.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use the @/* path alias for imports from ./src/*
Use the provided Supabase clients (import from src/lib/supabase/client.ts on the browser and src/lib/supabase/server.ts on the server) and types from src/lib/supabase/types.ts; do not instantiate ad-hoc clients

Files:

  • src/lib/poker/types.ts
  • src/lib/poker/csv-export.ts
  • src/components/poker/ExportButton.tsx
  • src/components/poker/SessionSummary.tsx
  • src/app/poker/[sessionUrl]/page.tsx
  • src/hooks/use-poker-statistics.ts
  • src/lib/poker/statistics.ts
  • src/lib/poker/actions.ts
  • src/components/poker/VoteResults.tsx
src/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

In App Router components, add the "use client" directive only where client-side interactivity is required

Files:

  • src/components/poker/ExportButton.tsx
  • src/components/poker/SessionSummary.tsx
  • src/app/poker/[sessionUrl]/page.tsx
  • src/components/poker/VoteResults.tsx
src/app/poker/**

📄 CodeRabbit inference engine (CLAUDE.md)

Keep Planning Poker feature routes and code under src/app/poker/

Files:

  • src/app/poker/[sessionUrl]/page.tsx
🧬 Code graph analysis (8)
src/lib/poker/csv-export.ts (1)
src/lib/poker/types.ts (2)
  • ExportData (194-207)
  • SessionStatistics (171-184)
src/components/poker/ExportButton.tsx (3)
src/lib/poker/types.ts (1)
  • PokerSession (24-50)
src/hooks/use-poker-statistics.ts (1)
  • useSessionStatistics (20-34)
src/lib/poker/csv-export.ts (3)
  • prepareExportData (73-96)
  • exportSessionToCSV (10-65)
  • downloadCSV (122-137)
src/components/poker/SessionSummary.tsx (1)
src/hooks/use-poker-statistics.ts (1)
  • useSessionStatistics (20-34)
src/app/poker/[sessionUrl]/page.tsx (2)
src/components/poker/ExportButton.tsx (1)
  • ExportButton (17-89)
src/components/poker/SessionSummary.tsx (1)
  • SessionSummary (20-264)
src/hooks/use-poker-statistics.ts (2)
src/lib/poker/types.ts (1)
  • SessionStatistics (171-184)
src/lib/poker/actions.ts (1)
  • getSessionStatistics (1073-1158)
src/lib/poker/statistics.ts (1)
src/lib/poker/types.ts (6)
  • PokerStory (52-64)
  • PokerVote (79-88)
  • PokerParticipant (66-77)
  • StoryStatistics (158-169)
  • SessionStatistics (171-184)
  • ParticipantStatistics (186-192)
src/lib/poker/actions.ts (3)
src/lib/poker/types.ts (5)
  • SessionStatistics (171-184)
  • EstimationSequenceType (3-3)
  • PokerVote (79-88)
  • PokerParticipant (66-77)
  • PokerStory (52-64)
src/lib/poker/utils.ts (1)
  • getSequenceByType (38-46)
src/lib/poker/statistics.ts (1)
  • calculateSessionStatistics (167-292)
src/components/poker/VoteResults.tsx (1)
src/lib/poker/statistics.ts (1)
  • calculateConsensusPercentage (20-73)
🔇 Additional comments (1)
src/app/poker/[sessionUrl]/page.tsx (1)

39-50: Nice integration of analytics controls.

The revised header cleanly surfaces the description and wires in the export control without disrupting the existing layout.

Comment on lines +31 to +62
lines.push("Story Details");
lines.push(
[
"Title",
"Description",
"Final Estimate",
"Vote Count",
"Average Vote",
"Median Vote",
"Consensus %",
"Time (min)",
"Participants",
"Votes",
].join(",")
);

exportData.stories.forEach((story) => {
const row = [
escapeCsvField(story.title),
escapeCsvField(story.finalEstimate || ""),
story.voteCount.toString(),
story.averageVote !== null ? story.averageVote.toFixed(1) : "N/A",
story.medianVote !== null ? story.medianVote.toString() : "N/A",
story.consensusPercentage.toFixed(1) + "%",
story.estimationTimeMinutes !== null
? story.estimationTimeMinutes.toFixed(1)
: "N/A",
escapeCsvField(story.participants.join("; ")),
escapeCsvField(story.votes.map((v) => `${v.participantName}:${v.voteValue}`).join("; ")),
];
lines.push(row.join(","));
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix CSV column misalignment before shipping.

The header advertises a “Description” column, but the row data never populates it—so every value after the title shifts one column to the left (final estimates show under “Description”, vote counts under “Final Estimate”, etc.). That makes the exported CSV misleading for any consumer. Drop the unused column (or populate it) so the column order and data line up.

   lines.push("Story Details");
   lines.push(
     [
       "Title",
-      "Description",
       "Final Estimate",
       "Vote Count",
       "Average Vote",
       "Median Vote",
       "Consensus %",
       "Time (min)",
       "Participants",
       "Votes",
     ].join(",")
   );

   exportData.stories.forEach((story) => {
     const row = [
       escapeCsvField(story.title),
       escapeCsvField(story.finalEstimate || ""),
       story.voteCount.toString(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lines.push("Story Details");
lines.push(
[
"Title",
"Description",
"Final Estimate",
"Vote Count",
"Average Vote",
"Median Vote",
"Consensus %",
"Time (min)",
"Participants",
"Votes",
].join(",")
);
exportData.stories.forEach((story) => {
const row = [
escapeCsvField(story.title),
escapeCsvField(story.finalEstimate || ""),
story.voteCount.toString(),
story.averageVote !== null ? story.averageVote.toFixed(1) : "N/A",
story.medianVote !== null ? story.medianVote.toString() : "N/A",
story.consensusPercentage.toFixed(1) + "%",
story.estimationTimeMinutes !== null
? story.estimationTimeMinutes.toFixed(1)
: "N/A",
escapeCsvField(story.participants.join("; ")),
escapeCsvField(story.votes.map((v) => `${v.participantName}:${v.voteValue}`).join("; ")),
];
lines.push(row.join(","));
});
lines.push("Story Details");
lines.push(
[
"Title",
"Final Estimate",
"Vote Count",
"Average Vote",
"Median Vote",
"Consensus %",
"Time (min)",
"Participants",
"Votes",
].join(",")
);
exportData.stories.forEach((story) => {
const row = [
escapeCsvField(story.title),
escapeCsvField(story.finalEstimate || ""),
story.voteCount.toString(),
story.averageVote !== null ? story.averageVote.toFixed(1) : "N/A",
story.medianVote !== null ? story.medianVote.toString() : "N/A",
story.consensusPercentage.toFixed(1) + "%",
story.estimationTimeMinutes !== null
? story.estimationTimeMinutes.toFixed(1)
: "N/A",
escapeCsvField(story.participants.join("; ")),
escapeCsvField(
story.votes
.map((v) => `${v.participantName}:${v.voteValue}`)
.join("; ")
),
];
lines.push(row.join(","));
});
🤖 Prompt for AI Agents
In src/lib/poker/csv-export.ts around lines 31 to 62 the CSV header includes a
"Description" column but the per-story row omits the story description causing
every subsequent field to shift left; fix this by inserting the story
description into the row as the second element (e.g.
escapeCsvField(story.description || "")) so the row fields align with the
header, keeping the rest of the columns in the same order and ensuring proper
escaping for the description.

Comment on lines +83 to +94
session: {
title: session.title,
description: session.description,
createdAt: new Date(session.created_at).toLocaleString(),
estimationSequence: session.estimation_sequence,
},
stories: statistics.storyStats,
summary: {
totalStories: statistics.estimatedStories,
averageTime: statistics.averageEstimationTimeMinutes,
consensusRate: statistics.overallConsensusRate,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the actual total story count in the export summary.

The CSV “Total Stories” metric currently repeats estimatedStories, so skipped/pending stories disappear from the summary. Swap in statistics.totalStories to report the real count.

   summary: {
-    totalStories: statistics.estimatedStories,
+    totalStories: statistics.totalStories,
     averageTime: statistics.averageEstimationTimeMinutes,
     consensusRate: statistics.overallConsensusRate,
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
session: {
title: session.title,
description: session.description,
createdAt: new Date(session.created_at).toLocaleString(),
estimationSequence: session.estimation_sequence,
},
stories: statistics.storyStats,
summary: {
totalStories: statistics.estimatedStories,
averageTime: statistics.averageEstimationTimeMinutes,
consensusRate: statistics.overallConsensusRate,
},
session: {
title: session.title,
description: session.description,
createdAt: new Date(session.created_at).toLocaleString(),
estimationSequence: session.estimation_sequence,
},
stories: statistics.storyStats,
summary: {
totalStories: statistics.totalStories,
averageTime: statistics.averageEstimationTimeMinutes,
consensusRate: statistics.overallConsensusRate,
},
🤖 Prompt for AI Agents
In src/lib/poker/csv-export.ts around lines 83 to 94, the export summary's
totalStories currently uses statistics.estimatedStories instead of the actual
count; replace statistics.estimatedStories with statistics.totalStories in the
summary object so the "Total Stories" field reports the real total (including
skipped/pending) rather than only estimated ones.

- Show consensus percentage for non-numeric sequences (T-shirt sizes, custom emojis)
- Fix array mutation in SessionSummary participant sorting
- Clone array before sorting to preserve React Query cache immutability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@TheEagleByte TheEagleByte merged commit 4885916 into main Oct 1, 2025
3 of 4 checks passed
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/components/poker/SessionSummary.tsx (2)

23-32: Consider providing more context in error state.

While the error handling is functional, consider exposing error details to help with debugging, especially for facilitators who may need to report issues.

You could enhance the error message:

-          <CardDescription>Failed to load session statistics</CardDescription>
+          <CardDescription>
+            Failed to load session statistics. Please try refreshing the page.
+          </CardDescription>

229-260: Consider adding ARIA labels for accessibility.

The estimate distribution visualization is well-implemented with smooth animations and clear percentage displays. For improved accessibility, consider adding ARIA attributes to the progress bars.

You could enhance accessibility:

                    <div className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2 overflow-hidden">
                      <div
                        className="h-full bg-indigo-500 transition-all duration-500"
                        style={{ width: `${percentage}%` }}
+                       role="progressbar"
+                       aria-valuenow={percentage}
+                       aria-valuemin={0}
+                       aria-valuemax={100}
+                       aria-label={`${estimate.estimate}: ${percentage.toFixed(0)}%`}
                      />
                    </div>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffce434 and 969911c.

📒 Files selected for processing (2)
  • src/components/poker/SessionSummary.tsx (1 hunks)
  • src/components/poker/VoteResults.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

In App Router components, add the "use client" directive only where client-side interactivity is required

Files:

  • src/components/poker/SessionSummary.tsx
  • src/components/poker/VoteResults.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use the @/* path alias for imports from ./src/*
Use the provided Supabase clients (import from src/lib/supabase/client.ts on the browser and src/lib/supabase/server.ts on the server) and types from src/lib/supabase/types.ts; do not instantiate ad-hoc clients

Files:

  • src/components/poker/SessionSummary.tsx
  • src/components/poker/VoteResults.tsx
🧬 Code graph analysis (2)
src/components/poker/SessionSummary.tsx (1)
src/hooks/use-poker-statistics.ts (1)
  • useSessionStatistics (20-34)
src/components/poker/VoteResults.tsx (2)
src/lib/poker/statistics.ts (1)
  • calculateConsensusPercentage (20-73)
src/lib/utils.ts (1)
  • cn (4-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests (20.x)
🔇 Additional comments (26)
src/components/poker/SessionSummary.tsx (16)

1-18: LGTM!

The imports and component setup follow Next.js App Router conventions correctly. The "use client" directive is appropriate for a component using React hooks, and all imports use the proper @/* path alias.


55-72: LGTM!

The empty state handling is well-implemented with a clear, user-friendly message that explains when statistics will be available.


86-192: LGTM!

The KPI grid is well-structured with responsive design, proper null checks, and clear visual hierarchy. The conditional rendering ensures only relevant metrics are displayed, and the color coding effectively distinguishes different metric types.


194-227: LGTM!

The participant breakdown is well-implemented with proper sorting and conditional display of statistics. The layout is clean and informative.


1-18: LGTM!

Imports are correctly structured, using the @/* alias as per guidelines, and the "use client" directive is appropriately placed for a component with client-side interactivity.


23-32: LGTM!

Error state handling is appropriate, clearly indicating the failure to load statistics.


34-53: LGTM!

Loading skeleton provides good UX with 6 placeholder cards matching the expected KPI grid layout.


55-72: LGTM!

Empty state correctly handles the case when no stories have been estimated yet, providing helpful guidance to users.


90-106: LGTM!

Stories completed metric correctly displays the ratio and conditionally shows pending/skipped counts when applicable.


108-126: LGTM!

Average time per story metric safely checks for null before rendering and displays both average and median when available.


128-142: LGTM!

Velocity metric is properly guarded with null check and displays stories per hour with clear labeling.


144-156: LGTM!

Consensus rate is displayed with appropriate styling and clear percentage formatting.


158-172: LGTM!

Active participants metric correctly checks for non-empty participantStats before rendering.


174-191: Verify array bounds before accessing mostCommonEstimates[0].

Line 184 and lines 187-188 access mostCommonEstimates[0] without verifying the array length is at least 1. Although line 175 checks that the length is > 0, it's safer to explicitly guard against potential race conditions or data inconsistencies.

Apply this diff to add explicit bounds checking:

           {statistics.mostCommonEstimates.length > 0 && (
             <div className="p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
               <div className="flex items-center gap-2 mb-2">
                 <BarChart3 className="h-4 w-4 text-indigo-600" />
                 <p className="text-sm font-medium text-slate-600 dark:text-slate-400">
                   Most Common Estimate
                 </p>
               </div>
-              <p className="text-2xl font-bold text-slate-900 dark:text-slate-100">
-                {statistics.mostCommonEstimates[0].estimate}
-              </p>
-              <p className="text-xs text-slate-500 mt-1">
-                Used {statistics.mostCommonEstimates[0].count}{" "}
-                {statistics.mostCommonEstimates[0].count === 1 ? "time" : "times"}
-              </p>
+              {statistics.mostCommonEstimates[0] && (
+                <>
+                  <p className="text-2xl font-bold text-slate-900 dark:text-slate-100">
+                    {statistics.mostCommonEstimates[0].estimate}
+                  </p>
+                  <p className="text-xs text-slate-500 mt-1">
+                    Used {statistics.mostCommonEstimates[0].count}{" "}
+                    {statistics.mostCommonEstimates[0].count === 1 ? "time" : "times"}
+                  </p>
+                </>
+              )}
             </div>
           )}

195-227: LGTM!

Participant contributions section correctly sorts by total votes and safely displays participant data with null checks for averageVoteValue.


229-260: LGTM!

Estimate distribution visualization correctly maps over mostCommonEstimates, calculates percentages, and renders progress bars with smooth transitions.

src/components/poker/VoteResults.tsx (10)

13-13: LGTM!

The import follows the correct path alias convention and brings in the necessary statistics utility.


30-30: LGTM!

The type extension is appropriate and maintains type safety throughout the component.


89-91: LGTM!

The consensus percentage calculation is correctly integrated into the vote analysis logic. The useMemo dependencies have been properly updated to include sequence.values, ensuring the calculation re-runs when the sequence changes.

Also applies to: 98-98, 102-102


200-247: LGTM!

The expanded statistics grid effectively incorporates the consensus metric with responsive design. The conditional styling at the 70% threshold provides clear visual feedback for high consensus, aligning with the PR objectives.


13-13: LGTM!

Import correctly uses the @/* alias and references the new statistics utility.


30-30: LGTM!

VoteAnalysis interface properly extended to include consensusPercentage as a number.


89-90: LGTM!

Consensus percentage calculation correctly delegates to the imported utility function with proper arguments.


98-98: LGTM!

consensusPercentage correctly included in the analysis return object.


102-102: LGTM!

useMemo dependencies correctly updated to include sequence.values, ensuring the consensus calculation re-runs when the sequence changes.


200-247: LGTM!

The statistics grid has been thoughtfully extended to 5 columns with proper responsive breakpoints. The consensus tile uses conditional styling (green highlight at ≥70%) that aligns with the PR's acceptance criteria for highlighting high consensus.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[EPIC-003] Story 7: Statistics & Analytics

2 participants