Files
encoach_frontend_new_v2/src/services/reports.service.ts
Yamen Ahmad 000969b0b9 feat(reports): replace mock Reports pages with real backend aggregates
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
2026-04-19 11:28:26 +04:00

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");
},
};