Skip to content

feat: phase 4.1 + 4.2 — multi-class compare statistics & activity dashboard#145

Open
NesiciCoding wants to merge 4 commits into
mainfrom
feature/phase-4-statistics-activity-dashboard
Open

feat: phase 4.1 + 4.2 — multi-class compare statistics & activity dashboard#145
NesiciCoding wants to merge 4 commits into
mainfrom
feature/phase-4-statistics-activity-dashboard

Conversation

@NesiciCoding

@NesiciCoding NesiciCoding commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Statistics — Compare tab: select up to 4 classes on a rubric → grouped average bar chart, per-criterion gap bar chart, multi-class trend line overlay, and collapsible rule-based Insights panel (struggling class < 55%, weak criterion ≥ 15 pp below class avg, inter-class divergence > 20 pp)
  • Statistics — test results: student view now shows submitted/graded test scores; class/rubric view shows a per-test average bar chart
  • Statistics — track/year filters: filter the class selector by Class.voTrack and Class.year
  • Activity Dashboard (/activity-dashboard): rubric × test × essay row grid against class columns with Link/Unlink, Assign All, and Open cell actions
  • New utilities: classComparisonAggregator.ts + activityDashboardAggregator.ts, each with 43 unit tests (incl. stress datasets)
  • New component: MultiClassTrendChart (Recharts LineChart, one <Line> per class with auto-colour cycle)
  • e2e: 22-statistics-compare.spec.ts seeds 3 classes at distinct performance levels and asserts bar chart, class names, bar count, insights panel, and trend line
  • i18n: all 5 locales updated (statistics.compare.*, statistics.insights.*, statistics.filters.*, activityDashboard.*)
  • Docs/README/LandingPage updated per project rule

Test plan

  • npm run typecheck — zero errors
  • npm test -- --run — all unit tests pass (incl. 86 new tests)
  • Statistics → Compare tab → select 2 classes + rubric → bar chart, criterion gap, trend, insights visible
  • Statistics → By Student → select a student with only test submissions → test results table shown (no empty-state)
  • Statistics → By Rubric → select a class with test submissions → test averages bar chart appears below trend
  • Activity Dashboard → link rubric to class → Class.rubricIds updated → unlink → removed
  • Activity Dashboard → Assign All essay to class with students → essay assignments created
  • Activity Dashboard → Open test → navigates to /tests/:id
  • npx playwright test e2e/specs/22-statistics-compare.spec.ts — 5 tests × 3 browsers pass

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Activity Dashboard page: organize rubrics, tests, and essays by class with link and assignment management.
    • Added Compare view in Statistics: compare class performance across multiple classes with bar charts, trend analysis, and performance insights.
    • Added Test Results display in the Statistics dashboard showing student scores and class averages.
  • Documentation

    • Updated README and in-app documentation with new routes and feature descriptions.
  • Tests

    • Added end-to-end tests for Statistics Compare functionality.

…hboard

- Statistics: new Compare tab with grouped avg bar chart, per-criterion gap
  chart, multi-class trend overlay, and rule-based Insights panel (struggling
  class, weak criterion, divergence thresholds)
- Statistics: test results now tracked — student view shows submitted/graded
  test scores; class view shows per-test average bar chart
- Statistics: track/year filter dropdowns scope the class selector
- Activity Dashboard: new /activity-dashboard page — rubric/test/essay × class
  grid with Link/Unlink, Assign All, and Open actions
- New utilities: classComparisonAggregator.ts, activityDashboardAggregator.ts
  (43 unit tests each, including stress-test datasets)
- New component: MultiClassTrendChart (Recharts LineChart, one line per class)
- e2e: 22-statistics-compare.spec.ts seeds 3 classes (A 100%, B 62%, C 25%)
  and asserts bar chart, class names, bar count, insights panel, trend line
- i18n: all 5 locales updated (statistics.compare.*, activityDashboard.*)
- Docs/README/LandingPage updated per CLAUDE.md documentation rule

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c68e1a08-a1a3-4681-a6d0-1b4eeb598870

📥 Commits

Reviewing files that changed from the base of the PR and between d3d466c and cd61e57.

📒 Files selected for processing (15)
  • e2e/specs/22-statistics-compare.spec.ts
  • src/components/Statistics/MultiClassTrendChart.tsx
  • src/locales/de.json
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/fr.json
  • src/locales/nl.json
  • src/pages/ActivityDashboardPage.tsx
  • src/pages/DocsPage.tsx
  • src/pages/StatisticsPage.tsx
  • src/types/index.ts
  • src/utils/activityDashboardAggregator.test.ts
  • src/utils/activityDashboardAggregator.ts
  • src/utils/classComparisonAggregator.test.ts
  • src/utils/classComparisonAggregator.ts
 ________________________________________________________________
< First, solve the problem. Then, write the code. - John Johnson >
 ----------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
📝 Walkthrough

Walkthrough

Adds two new features: a Compare mode in StatisticsPage with multi-class rubric comparison charts, per-criterion gap analysis, trend visualization, and an insights panel; and a new Activity Dashboard page (/activity-dashboard) that grids rubrics, tests, and essays against classes with link/unlink and assign actions. Both features include aggregation utilities, tests, i18n strings for five locales, routing, sidebar, docs, and landing page updates.

Changes

Statistics Compare Mode

Layer / File(s) Summary
Class comparison aggregation utilities
src/utils/classComparisonAggregator.ts, src/utils/classComparisonAggregator.test.ts
Defines ClassComparisonResult, MultiTrendPoint, and Insight interfaces. Implements compareClasses (weighted averages, medians, per-criterion percentages), buildMultiClassTrend (time-ordered multi-series trend data), and getInsights (struggling/weak-criterion/divergence detection), each with comprehensive Vitest coverage including boundary and stress tests.
MultiClassTrendChart component
src/components/Statistics/MultiClassTrendChart.tsx
New Recharts LineChart component that renders percentage-based multi-series trend data, with a CSS-variable color palette, customized tooltip using classNames, and a Legend. Returns null for datasets with fewer than 2 points.
StatisticsPage compare mode and student test results
src/pages/StatisticsPage.tsx
Adds a compare view mode with track/year filters, compare rubric selector, up to-4-class checkbox picker, and renders class average bar chart, per-criterion gap chart, MultiClassTrendChart, and collapsible insights panel. Also adds student-view test results table and rubric-view class test averages bar chart.
E2E tests for Compare tab
e2e/specs/22-statistics-compare.spec.ts
Playwright suite seeds three classes with predictable rubric scores and verifies Compare UI visibility, bar chart rendering for 2 and 3 classes, insights panel content for divergent performance, and trend chart presence.

Activity Dashboard

Layer / File(s) Summary
Activity aggregator utility
src/utils/activityDashboardAggregator.ts, src/utils/activityDashboardAggregator.test.ts
Exports ActivityKind, ActivityRow, CellData types, getActivityRows (deduplicates essay assignments by teacherKey), and buildDashboardMatrix (nested activityId → classId → CellData structure with kind-specific submittedCount/isLinked logic). Full Vitest coverage including stress tests.
ActivityDashboardPage
src/pages/ActivityDashboardPage.tsx
New page component with year/track filter state, visibleClasses derivation, matrix computation, toggleRubricLink and assignEssayToClass handlers, empty-state view, and a scrollable activity × class table with sticky columns, section header rows, and kind-specific action buttons (link/unlink, assign/all-assigned, open).
Route wiring, sidebar, i18n, docs, and landing
src/App.tsx, src/components/Layout/Sidebar.tsx, src/locales/*.json, src/pages/DocsPage.tsx, src/pages/LandingPage.tsx, README.md, .gitignore
Lazy-loads and routes ActivityDashboardPage at /activity-dashboard; adds LayoutGrid sidebar nav entry; extends all five locale files with activity_dashboard navigation label, statistics compare/test strings, and activityDashboard action strings; updates DocsPage route tree and analytics tab copy; updates landing page feature description; adds README route entries; adds daemon.pid to .gitignore.

Sequence Diagram(s)

sequenceDiagram
  actor Teacher
  participant StatisticsPage
  participant compareClasses
  participant buildMultiClassTrend
  participant getInsights
  participant MultiClassTrendChart

  Teacher->>StatisticsPage: Select "Compare" mode, pick rubric + classes
  StatisticsPage->>compareClasses: rubrics, studentRubrics, selectedClassIds, gradeScale
  compareClasses-->>StatisticsPage: ClassComparisonResult[] (avg, median, criterionAvg)
  StatisticsPage->>buildMultiClassTrend: selectedClassIds, rubrics, studentRubrics, classNames
  buildMultiClassTrend-->>StatisticsPage: MultiTrendPoint[] (time-ordered per-class scores)
  StatisticsPage->>getInsights: ClassComparisonResult[], rubricCriteria
  getInsights-->>StatisticsPage: Insight[] (struggling / weak_criterion / divergence)
  StatisticsPage->>MultiClassTrendChart: data, classIds, classNames
  MultiClassTrendChart-->>Teacher: Renders multi-series line chart
  Teacher->>StatisticsPage: Click Insights expander
  StatisticsPage-->>Teacher: Shows collapsible insight messages
Loading
sequenceDiagram
  actor Teacher
  participant ActivityDashboardPage
  participant getActivityRows
  participant buildDashboardMatrix
  participant useApp

  Teacher->>ActivityDashboardPage: Navigate to /activity-dashboard
  ActivityDashboardPage->>useApp: rubrics, tests, essayAssignments, classes, students, studentRubrics, studentTests
  ActivityDashboardPage->>getActivityRows: rubrics, tests, essayAssignments
  getActivityRows-->>ActivityDashboardPage: ActivityRow[] (deduplicated by teacherKey for essays)
  ActivityDashboardPage->>buildDashboardMatrix: activities, classes, students, studentRubrics, studentTests, essayAssignments
  buildDashboardMatrix-->>ActivityDashboardPage: matrix[activityId][classId] = CellData
  ActivityDashboardPage-->>Teacher: Renders activity × class grid with submittedCount badges
  Teacher->>ActivityDashboardPage: Click Link/Unlink on rubric cell
  ActivityDashboardPage->>useApp: updateClass with toggled rubricIds
  Teacher->>ActivityDashboardPage: Click Assign on essay cell
  ActivityDashboardPage->>useApp: createEssayAssignment for each unassigned student
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • NesiciCoding/RubricMaker#112: Introduced the DocsPage.tsx documentation system and route tree that this PR extends with the /activity-dashboard entry and updated analytics tab copy.

Poem

🐇 Hop hop, a dashboard grid appears,
Three classes compared — the data clears!
Insights bloom where rubrics meet,
Essays assigned with a button beat.
The rabbit charts trends, high and low,
Statistics flourish, watch them grow! 📊

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately summarizes the two main features introduced: multi-class compare statistics and activity dashboard, which aligns with the primary changes across the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/phase-4-statistics-activity-dashboard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 59.65% (🎯 52%) 4835 / 8105
🔵 Statements 58.31% (🎯 51%) 5486 / 9407
🔵 Functions 48.23% (🎯 42%) 1599 / 3315
🔵 Branches 50.43% (🎯 43%) 4011 / 7953
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/Layout/Sidebar.tsx 96% 86.3% 100% 95.65% 51
src/components/Statistics/MultiClassTrendChart.tsx 12.5% 0% 0% 14.28% 14-56
src/pages/ActivityDashboardPage.tsx 0% 0% 0% 0% 10-345
src/pages/DocsPage.tsx 0% 0% 0% 0% 45-1225
src/pages/LandingPage.tsx 69.69% 82.75% 84.61% 68.75% 131-138, 294, 323
src/pages/StatisticsPage.tsx 35.39% 18.39% 26.23% 36.46% 29-49, 73, 74, 103-108, 113-114, 127, 132, 135, 136, 138-139, 147, 151, 152, 154-155, 169, 171, 172, 174-175, 181, 184, 187, 191, 193, 196, 201, 206-238, 251-259, 264-269, 275, 297, 315-326, 328, 330, 335, 350-354, 361-373, 379-400, 406, 407, 410-426, 431-437, 441-479, 483, 486-519, 526-544, 563-1605
src/utils/activityDashboardAggregator.ts 100% 100% 100% 100%
src/utils/classComparisonAggregator.ts 98.64% 88.63% 96.29% 100% 89
Generated in workflow #483 for commit e59d726 by the Vitest Coverage Report Action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

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

⚠️ Outside diff range comments (1)
README.md (1)

46-56: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Update the Features section to describe the Statistics Compare mode and Activity Dashboard.

The Routes table (lines 151–152) was correctly updated with both new routes, but per the coding guideline, the Features section must also reflect major new capabilities. Currently:

  1. Line 47 (Statistics dashboard) does not mention the new multi-class comparison, insights panel, or trend visualization added in phase 4.1.
  2. Activity Dashboard (new in phase 4.2) is not described anywhere in the Features section (lines 5–74).

Expand the "5. Analytics & Reporting" section to highlight:

  • Multi-class rubric comparison (grouped averages, criterion gaps, trend overlay, insights panel with struggling-class and weak-criteria detection)
  • Activity Dashboard (grid view of rubrics/tests/essays against classes with link/unlink and assign actions)

This ensures README documentation is complete and discoverable for new users.

Also applies to: 151-152

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 46 - 56, The Analytics & Reporting section in the
Features section of README.md is incomplete. The Statistics dashboard bullet
point (line 47) needs to be expanded to include the new multi-class comparison
capabilities such as grouped averages, criterion gaps, trend overlay, and
insights panel with struggling-class and weak-criteria detection. Additionally,
add a new bullet point to describe the Activity Dashboard feature (new in phase
4.2) which provides a grid view of rubrics/tests/essays against classes with
link/unlink and assign actions. Ensure these additions appear in the Features
section (lines 5-74) to maintain documentation consistency with the Routes table
that already lists these new routes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/specs/22-statistics-compare.spec.ts`:
- Around line 76-95: The test `class averages reflect grade data (A > B > C)`
currently only verifies that class labels appear and that three bars exist, but
does not validate the actual average ordering. Add explicit assertions after the
bars check to verify either the displayed numeric values (Class A should be 100,
Class B should be between them, Class C should be 25) or the bar
heights/geometry to confirm they are in descending order, ensuring the test
actually guards against incorrect average computation or bar ordering.
- Around line 16-53: The trend chart requires at least 2 rubrics to render
because multiTrendData returns an array of trend points per rubric, and the
chart gates on checks for data.length >= 2. Currently, the beforeEach fixture
only seeds one rubric (cmp-rubric), so the chart will not render and the
assertion at line 123 checking for visible recharts-line will fail. Add a second
rubric to the beforeEach fixture using buildRubric (similar to how cmp-rubric is
created) and include it in the rm_rubrics array passed to seedStorage, ensuring
the trend chart has sufficient data to render.

In `@src/pages/ActivityDashboardPage.tsx`:
- Line 100: Remove all explanatory comments that describe what the code does
rather than why it exists, as they violate the repo's comment policy.
Specifically, remove the comments at lines 100, 133, 195, 218, 244, 276, and 289
(such as {/* Filter bar */}) since their meaning is already clear from the code
identifiers and structure. Keep only comments that explain non-obvious design
decisions or reasoning that cannot be inferred from reading the code itself.
- Around line 122-126: The option labels (VMBO-BB, VMBO-KB, VMBO-TL, HAVO, VWO)
in the select dropdown are hardcoded English strings that bypass the i18n
translation system. To fix this, first import the useTranslation hook at the top
of ActivityDashboardPage.tsx if not already present, then add translation keys
for each VO track label to src/locales/en.json (for example, voTracks.vmboBb,
voTracks.vmboKb, voTracks.vmboTl, voTracks.havo, voTracks.vwo), and finally
replace each hardcoded option label with a call to the t() function using the
corresponding key from your translation object.

In `@src/pages/DocsPage.tsx`:
- Around line 244-252: User-visible English strings are hardcoded directly in
the DocsPage.tsx configuration objects instead of being moved to translation
keys. Extract all hardcoded strings including the descriptions for the dashboard
entries and the label 'Activity Dashboard' from lines 244-252 and similar
sections, add them as key-value pairs to src/locales/en.json with descriptive
key names, then replace the hardcoded strings in the configuration objects with
references to those keys using the useTranslation hook. Ensure the component
imports and uses the useTranslation hook if not already present, and reference
the translation keys consistently throughout the affected sections.
- Line 251: Replace the raw hex color values `#0ea5e9` used in the style object
(appearing at line 251 and line 1007 in DocsPage.tsx) with a global CSS custom
property variable instead. Either map this color to an existing global CSS
variable like `--accent` if it matches the design intent, or define a new CSS
custom property centrally in your global CSS file and reference it using the
var() function to maintain theme consistency across the application.

In `@src/pages/LandingPage.tsx`:
- Line 47: The TEACHER_FEATURES.desc property contains hardcoded user-facing
text that violates localization guidelines. Extract this text and move it to a
new key in src/locales/en.json (e.g., something like
teacherFeaturesDescription), then import useTranslation hook at the top of the
LandingPage.tsx component if not already present, and replace the hardcoded desc
value with a call to t() using the new locale key. This ensures the landing page
content remains fully localized and consistent with the project's translation
standards.

In `@src/pages/StatisticsPage.tsx`:
- Around line 610-614: The hardcoded English text in the StatisticsPage
component violates localization guidelines. Replace all hardcoded user-visible
strings with localized keys using the t() function from useTranslation.
Specifically, replace the education track labels in the option elements
(VMBO-BB, VMBO-KB, VMBO-TL, HAVO, VWO) and the string "(max 4)" that is directly
appended at line 701 with appropriate t(...) calls referencing new locale keys
that should be added to src/locales/en.json. Ensure useTranslation is imported
and properly used throughout StatisticsPage.
- Around line 69-77: When the filteredClasses changes due to filterTrack or
filterYear updates in the useMemo hook, you need to synchronize the
selectedClassId and compareClassIds state to remove any class IDs that are no
longer present in the filtered list. Add a useEffect that watches the
filteredClasses dependency and verifies that selectedClassId and compareClassIds
only contain IDs that exist in filteredClasses. If selectedClassId or any IDs in
compareClassIds are no longer in filteredClasses, update these state variables
to remove the invalid IDs or reset them appropriately. This ensures that charts
only display classes that match the current filter criteria.

In `@src/utils/activityDashboardAggregator.test.ts`:
- Around line 243-265: Add a new test case within the 'matrix structure'
describe block that tests the scenario where a rubric and test share the same
ID. The test should create two activities with the same id value but different
kind values (e.g., both with id 'duplicate'), call buildDashboardMatrix with
these activities, and then assert that both entries remain independently
addressable by verifying that the matrix contains both activity entries and they
have not overwritten each other. This regression test ensures that the
buildDashboardMatrix function correctly handles cross-kind duplicate IDs without
silently losing data.

In `@src/utils/activityDashboardAggregator.ts`:
- Around line 3-15: Move the three type definitions ActivityKind, ActivityRow,
and CellData from src/utils/activityDashboardAggregator.ts to src/types/index.ts
to maintain centralized domain type definitions. After moving them, import these
types back into activityDashboardAggregator.ts from src/types/index.ts. This
ensures all domain-level shapes are defined in the centralized types location
and remain consistent across the codebase.
- Around line 52-53: The matrix initialization using matrix[activity.id] as the
key causes collisions when different activity kinds share the same ID. Replace
the single activity.id key with a composite key that combines both the activity
kind and ID (for example, creating a unique key string from both values) in the
activityDashboardAggregator.ts file at the matrix initialization point and any
other locations where matrix keys are accessed. Additionally, update all
corresponding lookups in src/pages/ActivityDashboardPage.tsx to use the same
composite key format to maintain consistency across both files.
- Around line 58-59: The variables submittedCount and isLinked are initialized
with default values on lines 58-59 but are unconditionally overwritten before
being used anywhere, triggering the no-useless-assignment lint error. Remove the
initial assignments of submittedCount = 0 and isLinked = false, and instead
declare these variables only when they are actually assigned with their final
values in the conditional branches that follow. This ensures each variable is
assigned exactly once with its meaningful value.

In `@src/utils/classComparisonAggregator.ts`:
- Around line 21-24: The Insight interface currently includes a hardcoded
message string field, but messages are being built as English text in the
classComparisonAggregator at lines 126, 134, and 159. Replace the message field
in the Insight interface with translationKey and params fields (or similar
structure), then update all three locations where insights are created to return
the translation key and any necessary parameters instead of building English
strings. The actual localization should be done in StatisticsPage using the
useTranslation hook and t(...) function to translate based on the key and
params.
- Around line 38-42: The studentRubrics filtering operation and similar
operations throughout the aggregators repeatedly call students.find() in nested
loops, causing linear lookups that scale poorly with large cohorts. Pre-index
the students array into a Map keyed by student ID before entering the filter
operations (at line 38-42 and similar locations at lines 82-86 and 99-103), then
replace each students.find((s) => s.id === sr.studentId) call with a direct Map
lookup to achieve O(1) performance instead of O(n) for each iteration.

---

Outside diff comments:
In `@README.md`:
- Around line 46-56: The Analytics & Reporting section in the Features section
of README.md is incomplete. The Statistics dashboard bullet point (line 47)
needs to be expanded to include the new multi-class comparison capabilities such
as grouped averages, criterion gaps, trend overlay, and insights panel with
struggling-class and weak-criteria detection. Additionally, add a new bullet
point to describe the Activity Dashboard feature (new in phase 4.2) which
provides a grid view of rubrics/tests/essays against classes with link/unlink
and assign actions. Ensure these additions appear in the Features section (lines
5-74) to maintain documentation consistency with the Routes table that already
lists these new routes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c2bd0457-fbb7-4e30-8521-6b60116ba48e

📥 Commits

Reviewing files that changed from the base of the PR and between 3a461e4 and d3d466c.

📒 Files selected for processing (19)
  • .gitignore
  • README.md
  • e2e/specs/22-statistics-compare.spec.ts
  • src/App.tsx
  • src/components/Layout/Sidebar.tsx
  • src/components/Statistics/MultiClassTrendChart.tsx
  • src/locales/de.json
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/fr.json
  • src/locales/nl.json
  • src/pages/ActivityDashboardPage.tsx
  • src/pages/DocsPage.tsx
  • src/pages/LandingPage.tsx
  • src/pages/StatisticsPage.tsx
  • src/utils/activityDashboardAggregator.test.ts
  • src/utils/activityDashboardAggregator.ts
  • src/utils/classComparisonAggregator.test.ts
  • src/utils/classComparisonAggregator.ts

Comment thread e2e/specs/22-statistics-compare.spec.ts
Comment thread e2e/specs/22-statistics-compare.spec.ts Outdated
Comment on lines +76 to +95
test('class averages reflect grade data (A > B > C)', async ({ appPage }) => {
await appPage.goto('/#/statistics');
await appPage.getByRole('button', { name: /compare/i }).click();

await appPage.getByLabel('Class A').check();
await appPage.getByLabel('Class B').check();
await appPage.getByLabel('Class C').check();

// Recharts renders tick labels with the avg value — Class A should show 100
// and Class C should show 25. We check for the presence of "100" and "25"
// somewhere in the chart region (the tooltip formatter shows "X%").
// Simpler: just assert all three class names appear in the results section.
await expect(appPage.getByText('Class A')).toBeVisible({ timeout: 5_000 });
await expect(appPage.getByText('Class B')).toBeVisible();
await expect(appPage.getByText('Class C')).toBeVisible();

// With 3 classes rendered, we should have 3 bar cells
const bars = appPage.locator('.recharts-bar-rectangle');
await expect(bars).toHaveCount(3, { timeout: 5_000 });
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This test does not actually validate “A > B > C” averages.

The assertions only verify labels and bar count; incorrect average computation/order would still pass. Please assert the displayed values/order (or bar geometry) explicitly so the test guards the stated behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/specs/22-statistics-compare.spec.ts` around lines 76 - 95, The test
`class averages reflect grade data (A > B > C)` currently only verifies that
class labels appear and that three bars exist, but does not validate the actual
average ordering. Add explicit assertions after the bars check to verify either
the displayed numeric values (Class A should be 100, Class B should be between
them, Class C should be 25) or the bar heights/geometry to confirm they are in
descending order, ensuring the test actually guards against incorrect average
computation or bar ordering.

Comment thread src/pages/ActivityDashboardPage.tsx Outdated
Comment thread src/pages/ActivityDashboardPage.tsx Outdated
Comment thread src/pages/DocsPage.tsx
Comment on lines +244 to +252
description: 'Grade distributions, per-criterion performance, class comparison, and trend analysis.',
color: '#64748b',
},
{
path: '/activity-dashboard',
label: 'Activity Dashboard',
description: 'Grid of all rubrics, tests, and essays vs classes — link, assign, and monitor coverage at a glance.',
color: '#0ea5e9',
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move new/updated docs copy to translation keys instead of inline English.

Line 244, Line 249, Line 250, Line 995, and Line 1009+ introduce user-visible English strings directly in TSX/config objects. This bypasses locale files and will leave these new docs updates untranslated.

As per coding guidelines, "All user-visible strings must use the useTranslation hook and a key from src/locales/en.json. Never hardcode English text in JSX."

Also applies to: 995-1005, 1007-1018

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/DocsPage.tsx` around lines 244 - 252, User-visible English strings
are hardcoded directly in the DocsPage.tsx configuration objects instead of
being moved to translation keys. Extract all hardcoded strings including the
descriptions for the dashboard entries and the label 'Activity Dashboard' from
lines 244-252 and similar sections, add them as key-value pairs to
src/locales/en.json with descriptive key names, then replace the hardcoded
strings in the configuration objects with references to those keys using the
useTranslation hook. Ensure the component imports and uses the useTranslation
hook if not already present, and reference the translation keys consistently
throughout the affected sections.

Source: Coding guidelines

Comment on lines +3 to +15
export type ActivityKind = 'rubric' | 'test' | 'essay';

export interface ActivityRow {
kind: ActivityKind;
id: string; // rubricId, testId, or teacherKey
name: string;
}

export interface CellData {
submittedCount: number;
totalStudents: number;
isLinked: boolean; // rubric in Class.rubricIds; essay has ≥1 assignment in this class
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move dashboard domain types to src/types/index.ts.

ActivityKind, ActivityRow, and CellData are domain-level shapes but are defined in this utility module. Centralizing them in src/types/index.ts keeps contracts consistent across page, utils, and tests.

As per coding guidelines, "All domain types must be defined in src/types/index.ts. Do not create ad-hoc inline types for data shapes already defined there."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/activityDashboardAggregator.ts` around lines 3 - 15, Move the three
type definitions ActivityKind, ActivityRow, and CellData from
src/utils/activityDashboardAggregator.ts to src/types/index.ts to maintain
centralized domain type definitions. After moving them, import these types back
into activityDashboardAggregator.ts from src/types/index.ts. This ensures all
domain-level shapes are defined in the centralized types location and remain
consistent across the codebase.

Source: Coding guidelines

Comment on lines +52 to +53
matrix[activity.id] = {};
for (const cls of classes) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Prevent matrix key collisions across activity kinds.

Using matrix[activity.id] can overwrite data when different kinds share the same ID (e.g., rubric id="r1" and test id="r1"). That corrupts dashboard cells and actions for one row with another row’s data.

💡 Suggested direction
+const getActivityKey = (activity: ActivityRow) => `${activity.kind}:${activity.id}`;

 for (const activity of activities) {
-    matrix[activity.id] = {};
+    const activityKey = getActivityKey(activity);
+    matrix[activityKey] = {};
     for (const cls of classes) {
       ...
-      matrix[activity.id][cls.id] = { submittedCount, totalStudents, isLinked };
+      matrix[activityKey][cls.id] = { submittedCount, totalStudents, isLinked };
     }
 }

Then update lookups in src/pages/ActivityDashboardPage.tsx to use the same composite key.

Also applies to: 84-84

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/activityDashboardAggregator.ts` around lines 52 - 53, The matrix
initialization using matrix[activity.id] as the key causes collisions when
different activity kinds share the same ID. Replace the single activity.id key
with a composite key that combines both the activity kind and ID (for example,
creating a unique key string from both values) in the
activityDashboardAggregator.ts file at the matrix initialization point and any
other locations where matrix keys are accessed. Additionally, update all
corresponding lookups in src/pages/ActivityDashboardPage.tsx to use the same
composite key format to maintain consistency across both files.

Comment thread src/utils/activityDashboardAggregator.ts Outdated
Comment on lines +21 to +24
export interface Insight {
kind: 'struggling' | 'weak_criterion' | 'divergence';
message: string;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace hardcoded insight messages with translation keys/params.

Line 126, Line 134, and Line 159 build end-user English text in a utility module. This makes insights non-localizable and leaks presentation concerns into domain logic. Return a translation key + params from this aggregator, then localize in StatisticsPage using t(...).

Suggested refactor (utility-side contract)
 export interface Insight {
     kind: 'struggling' | 'weak_criterion' | 'divergence';
-    message: string;
+    messageKey: string;
+    messageParams?: Record<string, string | number>;
 }
...
             insights.push({
                 kind: 'struggling',
-                message: `${r.className} may need targeted support — class average: ${r.average.toFixed(1)}%.`,
+                messageKey: 'statistics.insights.struggling',
+                messageParams: { className: r.className, average: r.average.toFixed(1) },
             });
...
                 insights.push({
                     kind: 'weak_criterion',
-                    message: `"${c.title}" is a consistent weak point in ${r.className} (${cAvg.toFixed(1)}% vs class avg ${r.average.toFixed(1)}%).`,
+                    messageKey: 'statistics.insights.weak_criterion',
+                    messageParams: {
+                        criterion: c.title,
+                        className: r.className,
+                        criterionAvg: cAvg.toFixed(1),
+                        classAvg: r.average.toFixed(1),
+                    },
                 });
...
             insights.push({
                 kind: 'divergence',
-                message: `"${maxGapCritTitle}" shows the biggest gap: ${maxGapHigh} is ${maxGap.toFixed(0)} pp ahead of ${maxGapLow}.`,
+                messageKey: 'statistics.insights.divergence',
+                messageParams: {
+                    criterion: maxGapCritTitle,
+                    highClass: maxGapHigh,
+                    gap: maxGap.toFixed(0),
+                    lowClass: maxGapLow,
+                },
             });

As per coding guidelines, “All user-visible strings must use the useTranslation hook and a key from src/locales/en.json.”

Also applies to: 124-127, 133-135, 157-160

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/classComparisonAggregator.ts` around lines 21 - 24, The Insight
interface currently includes a hardcoded message string field, but messages are
being built as English text in the classComparisonAggregator at lines 126, 134,
and 159. Replace the message field in the Insight interface with translationKey
and params fields (or similar structure), then update all three locations where
insights are created to return the translation key and any necessary parameters
instead of building English strings. The actual localization should be done in
StatisticsPage using the useTranslation hook and t(...) function to translate
based on the key and params.

Source: Coding guidelines

Comment on lines +38 to +42
const srs = studentRubrics.filter(
(sr) =>
sr.rubricId === rubricId &&
students.find((s) => s.id === sr.studentId)?.classId === classId
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Pre-index students by id to remove repeated linear lookups.

students.find(...) is called in nested loops in both aggregators (Line 41, Line 85, Line 102). This scales poorly with large cohorts and repeated recomputations in compare mode.

Low-impact optimization
 export function compareClasses(
 ...
 ): ClassComparisonResult[] {
+    const studentClassById = new Map(students.map((s) => [s.id, s.classId]));
     return classIds
         .map((classId) => {
 ...
             const srs = studentRubrics.filter(
                 (sr) =>
                     sr.rubricId === rubricId &&
-                    students.find((s) => s.id === sr.studentId)?.classId === classId
+                    studentClassById.get(sr.studentId) === classId
             );
 export function buildMultiClassTrend(
 ...
 ): MultiTrendPoint[] {
+    const studentClassById = new Map(students.map((s) => [s.id, s.classId]));
     const relevantRubrics = rubrics
         .filter((r) =>
             classIds.some((classId) =>
                 studentRubrics.some(
                     (sr) =>
                         sr.rubricId === r.id &&
-                        students.find((s) => s.id === sr.studentId)?.classId === classId
+                        studentClassById.get(sr.studentId) === classId
                 )
             )
         )
 ...
                         sr.rubricId === r.id &&
-                        students.find((s) => s.id === sr.studentId)?.classId === classId
+                        studentClassById.get(sr.studentId) === classId
                 )

Also applies to: 82-86, 99-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/classComparisonAggregator.ts` around lines 38 - 42, The
studentRubrics filtering operation and similar operations throughout the
aggregators repeatedly call students.find() in nested loops, causing linear
lookups that scale poorly with large cohorts. Pre-index the students array into
a Map keyed by student ID before entering the filter operations (at line 38-42
and similar locations at lines 82-86 and 99-103), then replace each
students.find((s) => s.id === sr.studentId) call with a direct Map lookup to
achieve O(1) performance instead of O(n) for each iteration.

… types, Map optimization, filter sync useEffect

- Composite key `kind:id` in dashboard matrix to prevent collision when rubric and test share the same id
- Move ActivityKind/ActivityRow/CellData/Insight to src/types/index.ts
- Insight interface uses messageKey+messageParams for i18n; StatisticsPage renders via t()
- Pre-index students with Map for O(1) lookup in compareClasses and buildMultiClassTrend
- Replace hardcoded VO track options with VO_TRACKS/VO_TRACK_LABELS in ActivityDashboardPage and StatisticsPage
- useEffect for filter sync placed after state declarations to avoid temporal dead zone
- Add 3-class compare e2e fixture with 2 rubrics to satisfy trend chart ≥2 points guard
- Update all tests to use composite matrix keys and messageKey/messageParams assertions

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
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.

1 participant