Skip to content

Commit 2a70fe8

Browse files
WangYuTenggclaude
andcommitted
fix(analytics): wire Grading Statistics card to real submission counts
Top Auto-graded / Pending Review / Total Graded tiles were hardcoded 0. Add /api/auto-grade/grading-stats returning: - autoGradedThisWeek: distinct submissions with AI-assisted marks in 7d - pendingReview: submissions in submitted/grading/late status - totalGraded: submissions in 'graded' status AnalyticsTab now fetches and renders, with em-dash placeholders while loading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a9e8cf6 commit 2a70fe8

3 files changed

Lines changed: 84 additions & 3 deletions

File tree

src/client/features/staff-grading/components/AnalyticsTab.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
1+
import { useEffect, useState } from 'react';
2+
import { apiClient } from '../../../lib/api';
13
import { CostAnalytics } from './CostAnalytics';
24
import { QueueMonitor } from './QueueMonitor';
35

6+
type GradingStats = {
7+
autoGradedThisWeek: number;
8+
pendingReview: number;
9+
totalGraded: number;
10+
};
11+
412
export function AnalyticsTab() {
13+
const [stats, setStats] = useState<GradingStats | null>(null);
14+
const [loading, setLoading] = useState(true);
15+
16+
useEffect(() => {
17+
apiClient<GradingStats>('/api/auto-grade/grading-stats')
18+
.then(setStats)
19+
.catch(console.error)
20+
.finally(() => setLoading(false));
21+
}, []);
22+
523
return (
624
<div className="space-y-6">
725
{/* Statistics */}
@@ -10,17 +28,23 @@ export function AnalyticsTab() {
1028
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1129
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
1230
<p className="text-sm text-green-600 font-medium mb-1">Auto-graded</p>
13-
<p className="text-3xl font-bold text-green-900">0</p>
31+
<p className="text-3xl font-bold text-green-900">
32+
{loading ? '—' : stats?.autoGradedThisWeek ?? 0}
33+
</p>
1434
<p className="text-xs text-green-700 mt-1">submissions this week</p>
1535
</div>
1636
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
1737
<p className="text-sm text-yellow-600 font-medium mb-1">Pending Review</p>
18-
<p className="text-3xl font-bold text-yellow-900">0</p>
38+
<p className="text-3xl font-bold text-yellow-900">
39+
{loading ? '—' : stats?.pendingReview ?? 0}
40+
</p>
1941
<p className="text-xs text-yellow-700 mt-1">submissions waiting</p>
2042
</div>
2143
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
2244
<p className="text-sm text-blue-600 font-medium mb-1">Total Graded</p>
23-
<p className="text-3xl font-bold text-blue-900">0</p>
45+
<p className="text-3xl font-bold text-blue-900">
46+
{loading ? '—' : stats?.totalGraded ?? 0}
47+
</p>
2448
<p className="text-xs text-blue-700 mt-1">all time</p>
2549
</div>
2650
</div>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Hono } from 'hono';
2+
import { and, eq, gte, sql } from 'drizzle-orm';
3+
import { db } from '../../../db/index.js';
4+
import { submissions, marks } from '../../../db/schema.js';
5+
import { authMiddleware, type AuthContext } from '../../middleware/auth.js';
6+
import { getErrorMessage } from '../../lib/error-utils.js';
7+
8+
const gradingStatsRoute = new Hono<AuthContext>();
9+
10+
/**
11+
* GET /api/auto-grade/grading-stats
12+
*
13+
* Returns top-line submission grading counts for the staff Analytics tab:
14+
* - autoGradedThisWeek: distinct submissions that received any AI-assisted mark in the last 7 days
15+
* - pendingReview: submissions awaiting human review (status submitted/grading)
16+
* - totalGraded: all-time count of submissions in 'graded' status
17+
*/
18+
gradingStatsRoute.get('/grading-stats', authMiddleware, async (c) => {
19+
const user = c.get('user');
20+
21+
if (!user || (user.role !== 'staff' && user.role !== 'admin')) {
22+
return c.json({ error: 'Unauthorized' }, 403);
23+
}
24+
25+
try {
26+
const sevenDaysAgo = new Date();
27+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
28+
29+
const [autoGradedRow] = await db
30+
.select({ value: sql<number>`COUNT(DISTINCT ${marks.submissionId})::int` })
31+
.from(marks)
32+
.where(and(eq(marks.isAiAssisted, true), gte(marks.createdAt, sevenDaysAgo)));
33+
34+
const [pendingRow] = await db
35+
.select({ value: sql<number>`COUNT(*)::int` })
36+
.from(submissions)
37+
.where(sql`${submissions.status} IN ('submitted', 'grading', 'late')`);
38+
39+
const [totalRow] = await db
40+
.select({ value: sql<number>`COUNT(*)::int` })
41+
.from(submissions)
42+
.where(eq(submissions.status, 'graded'));
43+
44+
return c.json({
45+
autoGradedThisWeek: autoGradedRow?.value ?? 0,
46+
pendingReview: pendingRow?.value ?? 0,
47+
totalGraded: totalRow?.value ?? 0,
48+
});
49+
} catch (error: unknown) {
50+
console.error('Grading stats error:', error);
51+
return c.json({ error: 'Failed to fetch grading stats', details: getErrorMessage(error) }, 500);
52+
}
53+
});
54+
55+
export default gradingStatsRoute;

src/server/routes/auto-grade/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import acceptRoute from './accept.js';
88
import rejectRoute from './reject.js';
99
import assignmentsRoute from './assignments.js';
1010
import batchAcceptRoute from './batch-accept.js';
11+
import gradingStatsRoute from './grading-stats.js';
1112

1213
const autoGrade = new Hono<AuthContext>();
1314

@@ -19,5 +20,6 @@ autoGrade.route('/', acceptRoute);
1920
autoGrade.route('/', rejectRoute);
2021
autoGrade.route('/', assignmentsRoute);
2122
autoGrade.route('/', batchAcceptRoute);
23+
autoGrade.route('/', gradingStatsRoute);
2224

2325
export default autoGrade;

0 commit comments

Comments
 (0)