Wire the three admin Reports pages (/admin/student-performance, /admin/stats-corporate, /admin/record) to a new encoach_lms_api/controllers/reports.py that aggregates from encoach.student.attempt. * /api/reports/student-performance: per-student band averages + CEFR, with entity / level / search filters. * /api/reports/stats-corporate: by_module bar, N-month trend line, CEFR distribution pie, entity comparison bar, threshold + entity filters and meta.attempts_considered for UI. * /api/reports/record: paginated attempt history with entity / user / period filters, EX-### exam codes, duration derived from start/end. * /api/reports/filters: shared picker returning only entities / students that actually have attempts. Frontend: new reports.service.ts, all three pages rewritten to hit these endpoints; Recharts graphs now read live data, CSV export added on Student Performance and Record. Seeding: seed_reports.py completes any in_progress attempts and backfills 6 months of historical attempts across 3 entities so the trend / distribution / KPI panels have meaningful data. Idempotent. Tests: test_reports_flows.py (25/25 PASS) covers shape, filters, pagination. Regressions still green: Configuration 24/24, Support 29/29, Training 26/26. Browser-verified on localhost:8080 with admin login — all 4 tabs in Stats Corporate render, Student Performance shows real students + KPIs, Record shows 28 attempts with filter + CSV export working. Docs: new §20 in PROJECT_SUMMARY.md documenting scope, artifacts, seeding, test results, and gotchas (encoach.exam.custom uses `title` not `name`; encoach.entity requires `code`; in_progress attempts are excluded from aggregates). Made-with: Cursor
138 lines
3.0 KiB
TypeScript
138 lines
3.0 KiB
TypeScript
import { api } from "@/lib/api-client";
|
|
|
|
export interface StudentPerformanceRow {
|
|
student_id: number;
|
|
student_name: string;
|
|
login: string;
|
|
entity_id: number | null;
|
|
entity_name: string;
|
|
reading: number | null;
|
|
listening: number | null;
|
|
writing: number | null;
|
|
speaking: number | null;
|
|
overall: number | null;
|
|
level: string | null;
|
|
attempts_count: number;
|
|
last_attempt_at: string | null;
|
|
}
|
|
|
|
export interface StudentPerformanceResponse {
|
|
items: StudentPerformanceRow[];
|
|
total: number;
|
|
}
|
|
|
|
export interface StatsModuleRow {
|
|
module: string;
|
|
score: number;
|
|
n: number;
|
|
}
|
|
|
|
export interface StatsTrendRow {
|
|
month: string;
|
|
avg: number;
|
|
period: string;
|
|
n: number;
|
|
}
|
|
|
|
export interface StatsDistributionRow {
|
|
name: string;
|
|
value: number;
|
|
color: string;
|
|
}
|
|
|
|
export interface StatsComparisonRow {
|
|
entity_id: number | null;
|
|
entity_name: string;
|
|
avg: number;
|
|
attempts: number;
|
|
}
|
|
|
|
export interface StatsCorporateResponse {
|
|
by_module: StatsModuleRow[];
|
|
trend: StatsTrendRow[];
|
|
distribution: StatsDistributionRow[];
|
|
comparison: StatsComparisonRow[];
|
|
meta: {
|
|
attempts_considered: number;
|
|
threshold: number;
|
|
months: number;
|
|
};
|
|
}
|
|
|
|
export interface RecordRow {
|
|
id: number;
|
|
student_id: number | null;
|
|
student_name: string;
|
|
assignment: string;
|
|
assignment_id: number | null;
|
|
exam: string;
|
|
exam_code: string;
|
|
exam_id: number | null;
|
|
entity_id: number | null;
|
|
entity_name: string;
|
|
date: string | null;
|
|
started_at: string | null;
|
|
completed_at: string | null;
|
|
score: number | null;
|
|
duration_min: number | null;
|
|
duration: string;
|
|
status: string;
|
|
status_label: string;
|
|
cefr_level: string | null;
|
|
}
|
|
|
|
export interface RecordResponse {
|
|
items: RecordRow[];
|
|
total: number;
|
|
page: number;
|
|
size: number;
|
|
}
|
|
|
|
export interface ReportsFiltersResponse {
|
|
entities: { id: number; name: string }[];
|
|
students: { id: number; name: string; login: string }[];
|
|
}
|
|
|
|
type QueryParams = Record<string, string | number | boolean | undefined>;
|
|
|
|
export const reportsService = {
|
|
async studentPerformance(params?: {
|
|
entity_id?: number;
|
|
level?: string;
|
|
search?: string;
|
|
since?: string;
|
|
}): Promise<StudentPerformanceResponse> {
|
|
return api.get<StudentPerformanceResponse>(
|
|
"/reports/student-performance",
|
|
params as QueryParams,
|
|
);
|
|
},
|
|
|
|
async statsCorporate(params?: {
|
|
entity_id?: number;
|
|
threshold?: number;
|
|
months?: number;
|
|
since?: string;
|
|
}): Promise<StatsCorporateResponse> {
|
|
return api.get<StatsCorporateResponse>(
|
|
"/reports/stats-corporate",
|
|
params as QueryParams,
|
|
);
|
|
},
|
|
|
|
async record(params?: {
|
|
user_id?: number;
|
|
entity_id?: number;
|
|
period?: "day" | "week" | "month";
|
|
status?: string;
|
|
page?: number;
|
|
size?: number;
|
|
}): Promise<RecordResponse> {
|
|
return api.get<RecordResponse>("/reports/record", params as QueryParams);
|
|
},
|
|
|
|
async filters(): Promise<ReportsFiltersResponse> {
|
|
return api.get<ReportsFiltersResponse>("/reports/filters");
|
|
},
|
|
};
|