Skip to content
TrackToImprove · Internal Engineering Docs

The Academy

Teach yourself this codebase chapter by chapter — architecture, EF Core, CQRS, background jobs, testing and ops — rendered live from docs/academy/.
– chapters – total read – diagrams 0/0 self-checks
95
API Endpoints
18
Entities
88
Use Cases
4
Roles
922
Tests
◆
Architecture
API Layer Controllers · Middleware · CORS Application Layer CQRS · MediatR · Validation · DTOs Domain Layer Entities · Enums · Interfaces Infrastructure EF Core · SQL · Hangfire External SMTP · Seq · Files HTTP 88 Use Cases 18 Entities
.NET 9 C# Clean Arch CQRS
▶
Request Pipeline
Client Request CORS FlutterApp origin Rate Limiting per role JWT + Localization ?lang=ar Security + Logging MediatR Pipeline ValidationBehavior → Handler → Repository → SQL Response (JSON / File)
■
Endpoints by Controller
◆
Domain Model — 18 Entities
User Id · FullName · Email · PasswordHash Role · IsActive · IsVerified Teacher NationalId · Bio · Rating Student StudentCode · ParentId Parent Subject Group Name · GradeLevel · Period GroupSchedule Session Date · Type · Topic · Headers SessionHeaderDefinition Label · ColumnType · MaxScore StudentSession Attended Assessment Score · MaxScore StudentsGroups Badge StudentsBadges Leaderboard Rank · Score PaymentRecord Notification UploadLog TPT TPT FK 1:N 1:N 1:N headers pivot via join →User
⚙
Background Jobs (Hangfire)
ProcessExcelJob
Legacy + Session-bound Excel parsing
Queue: uploads | strategy: error|replace|skip
LeaderboardJob
Hourly ranking updates
Recurring: hourly
BadgeAwardingJob
Daily badge evaluation
Recurring: daily
CleanupOldFilesJob
Delete files > 30 days
Recurring: daily

Teacher Journey — 50 Endpoints

Complete API reference grouped by domain. Every endpoint with real request/response examples.

🔒 Authentication 3 endpoints

Register Account

Create a teacher account. Returns tokens immediately but email must be verified before login.

Request / Response
Request Body
{
  "fullName": "Ahmed Hassan",
  "email": "ahmed@gmail.com",
  "username": "ahmed_h",
  "password": "Teacher@123",
  "phoneNumber": "+201012345678",
  "dateOfBirth": "1990-05-15T00:00:00",
  "role": "Teacher"
}
Response 201
{
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
    "expiresAt": "2026-09-12T12:15:00Z",
    "role": "Teacher",
    "userId": "b0000000-0000-00:00:00-000000000001"
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Public endpoint — no authentication required.
Rate Limit: 10 requests/minute.
Auth: None (public).
Response Fields: nationalId, subscriptionPlan (enum string: "Free"/"Pro"/"Premium"), subscriptionEnd (nullable DateTime), bio (nullable), rating (byte 0-5), subjectName (from linked Subject).
Errors: 401 (no/invalid token), 403 (wrong role).
Enums:
· role: "Teacher" | "Student" | "Parent" — Admin cannot self-register.
Validation:
· fullName: required, max 100 chars.
· email: required, valid format, must be unique.
· username: required, 3-50 chars, must be unique.
· password: required, min 8 chars.
· phoneNumber: required, E.164 format regex ^\+?\d{7,15}$.
· dateOfBirth: required, must be in the past.
Errors: 400 (validation), 409 (duplicate email/username).

Verify Email

Enter the 6-digit numeric code sent to your email. Code expires in 2 minutes.

Request / Response
Request Body
{ "email": "ahmed@gmail.com", "code": "482916" }
Response 200
{ "data": { "message": "success.emailVerified" }, "meta": { "timestamp": "...", "requestId": "..." } }
Prerequisites: Must have called POST /auth/register first. A 6-digit code was sent to the registered email.
Rate Limit: 10 requests/minute per IP.
Validation:
· email: required, valid format, must match a pending verification.
· code: required, exactly 6 numeric digits (^[0-9]{6}$).
Errors: 400 (validation), 401 (expired/invalid code — code expires in 2 minutes).
Resend: Call POST /auth/resend-verification with { "email": "..." } to get a new code.

Login

Authenticate with email + password. Returns JWT access (15min) + refresh (7 days) tokens.

Request / Response
Request Body
{ "email": "ahmed@gmail.com", "password": "Teacher@123" }
Response 200
{
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
    "expiresAt": "2026-09-12T12:15:00Z",
    "role": "Teacher",
    "userId": "b0000000-0000-0000-0000-000000000001"
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Must have verified email via POST /auth/verify-email.
Rate Limit: 10 requests/minute per IP.
Validation:
· email: required, valid format.
· password: required, min 6 chars (note: registration requires 8, login only 6 for backward compat).
Errors: 400 (validation), 401 (bad credentials OR unverified email).
Token Lifecycle: Access token = 15 minutes. Refresh token = 7 days. Use Bearer token in Authorization header for all subsequent requests. Refresh via POST /auth/refresh-token when access expires.
👤 Profile & Dashboard 5 endpoints

Get My Profile

View your full teacher profile including subscription status.

Request / Response
Response 200
{
  "data": {
    "id": "b0000000-0000-0000-0000-000000000001",
    "fullName": "Ahmed Hassan",
    "email": "ahmed@gmail.com",
    "username": "ahmed_h",
    "phoneNumber": "+201012345678",
    "dateOfBirth": "1990-05-15T00:00:00",
    "nationalId": "29005151234567",
    "subscriptionPlan": "Premium",
    "subscriptionEnd": "2027-09-12T00:00:00Z",
    "bio": "Biology teacher with 10 years experience",
    "rating": 5,
    "subjectName": "Biology",
    "createdAt": "2026-01-15T00:00:00Z"
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Query: search (optional, matches name/email), filter ("my-students" default | "available"), page (default 1), pageSize (default 20, max 100).
Behavior: the default "my-students" lists pending + active memberships in your groups (never platform-wide students). "available" lists students with no membership in your groups. Pending students report isEnrolled: false and zeroed metrics.
Errors: 401 (no token), 403 (wrong role), 400 (invalid filter/paging).

Update My Profile

Update your name, phone, bio, and subscription plan.

Request / Response
Request Body
{
  "fullName": "Ahmed Hassan",
  "phoneNumber": "+201012345678",
  "bio": "Biology teacher — 10 years experience",
  "subscriptionPlan": "Premium"
}
Response 200 — same shape as GET profile
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Validation: fullName (required, max 100), phoneNumber (required, E.164 regex). Email change triggers re-verification (old email stays active until new one is verified).
Errors: 400 (validation), 401 (no token), 403 (wrong role), 409 (duplicate email if changed).

Teacher Dashboard

Full dashboard: profile, overview stats, week-over-week trends, top/bottom groups, recent uploads, top students, platform comparison, students at risk. Fixed-size summary endpoint — no pagination. Cached 2 minutes.

Request / Response
Response 200 (key fields)
{
  "data": {
    "profile": { "fullName": "Ahmed Hassan", "email": "ahmed@gmail.com", "subject": "Biology", "subscriptionPlan": "Premium" },
    "overview": {
      "totalGroups": 8, "activeGroups": 6, "totalStudents": 142,
      "totalSessionsThisMonth": 18, "pendingPaymentsEGP": 3500.00,
      "totalUploadsThisMonth": 12, "scoreTrend": 4.5,
      "topPerformingGroup": "Biology - Secondary 2", "topPerformingGroupScore": "88.5%",
      "studentsAtRiskCount": 5, "uploadStreakWeeks": 6,
      "classAverageScore": 76.3, "totalSessionsAllTime": 234
    },
    "trends": {
      "scoreChangePercent": 5.2, "attendanceChangePercent": 3.1,
      "studentsChange": 8, "scoreTrend": "up", "attendanceTrend": "up"
    },
    "topGroups": {
      "top": [
        { "groupId": "...", "name": "Biology - Secondary 2", "subject": "Biology", "gradeLevel": "SecondaryTwo", "period": "First Term", "studentCount": 45, "averageScore": 88.5, "isActive": true, "attendanceRate": 94.0, "trend": "up" }
      ],
      "bottom": [
        { "groupId": "...", "name": "Math - Secondary 1", "subject": "Mathematics", "gradeLevel": "SecondaryOne", "period": "First Term", "studentCount": 22, "averageScore": 54.2, "isActive": true, "attendanceRate": 72.0, "trend": "down" }
      ]
    },
    "recentUploads": [
      { "uploadId": "...", "fileName": "session_2026-09-10.xlsx", "groupName": "Biology - Secondary 2", "status": "Completed", "rowsProcessed": 45, "uploadedAt": "2026-09-12T14:30:00Z" }
    ],
    "topStudents": [
      { "gradeLevel": "SecondaryTwo", "groupName": "Biology - Secondary 2", "students": [
        { "studentId": "...", "fullName": "Ali Hassan", "studentCode": "STU-003", "rank": 1, "avgScore": 95.5, "attendanceRate": 100.0 }
      ]}
    ],
    "unreadNotifications": 3,
    "platformComparison": { "yourAverageScore": 76.3, "schoolAverageScore": 71.0, "yourRank": 3, "totalTeachers": 15, "performanceLabel": "Above Average" }
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Caching: IMemoryCache, 2 minutes TTL. Cache key: teacher_home_{userId}.
Response Fields: profile, overview (incl. studentsAtRiskCount), trends (week-over-week), topGroups (top 2 + bottom 2 by average score), recentUploads (last 3 uploads with status), topStudents (grouped by grade level, top 3 per grade from best group), unreadNotifications, platformComparison (nullable).
No pagination — this is a fixed-size summary endpoint.
Errors: 401 (no token), 403 (wrong role).

Teacher Analytics & Charts Dashboard

Dedicated analytics endpoint returning rich datasets for Flutter charts: score timelines (line/area), attendance timelines (stacked bar), score distribution brackets (histogram/donut), student health status (gauge/pie), group performance comparisons (multi-bar), assessment category breakdowns (polar), grade level distribution, and platform benchmarks with multi-criteria filtering.

Request / Response
Query Params
?groupId=...&gradeLevel=SecondaryTwo&period=First%20Term&timeRange=30d&search=Ali
Response 200 (sample)
{
  "data": {
    "filterApplied": {
      "groupId": null, "groupName": null, "gradeLevel": null,
      "period": "First Term", "timeRange": "30d",
      "dateFrom": "2026-08-20T00:00:00Z", "dateTo": "2026-09-19T23:59:59Z", "search": null
    },
    "overview": {
      "totalGroups": 6, "activeGroups": 5, "totalStudents": 145,
      "totalSessionsInPeriod": 24, "averageClassScore": 78.4,
      "overallAttendanceRate": 89.6, "pendingPaymentsEGP": 3850.00,
      "studentsAtRiskCount": 4, "uploadStreakWeeks": 7
    },
    "trends": { "scoreChangePercent": 4.8, "attendanceChangePercent": 2.1, "studentsChange": 12, "scoreTrend": "up", "attendanceTrend": "up" },
    "scoreTimelineChart": [
      { "date": "2026-08-25T10:00:00Z", "label": "Aug 25", "sessionTitle": "Session 1", "groupName": "Physics - Sec 2", "averageScore": 75.5, "highestScore": 98.0, "lowestScore": 45.0, "attendanceRate": 92.0 }
    ],
    "attendanceTimelineChart": [
      { "date": "2026-08-25T10:00:00Z", "label": "Aug 25", "sessionTitle": "Session 1", "groupName": "Physics - Sec 2", "attendedCount": 46, "absentCount": 4, "totalStudents": 50, "attendanceRate": 92.0 }
    ],
    "scoreDistribution": {
      "excellent": { "count": 35, "percentage": 24.1, "range": "85% - 100%" },
      "veryGood": { "count": 52, "percentage": 35.9, "range": "75% - 84.9%" },
      "good": { "count": 38, "percentage": 26.2, "range": "65% - 74.9%" },
      "pass": { "count": 16, "percentage": 11.0, "range": "50% - 64.9%" },
      "struggling": { "count": 4, "percentage": 2.8, "range": "< 50%" },
      "totalEvaluated": 145
    },
    "studentHealthStatus": {
      "excelling": { "count": 35, "percentage": 24.1, "label": "Excelling" },
      "onTrack": { "count": 82, "percentage": 56.6, "label": "On Track" },
      "needsPush": { "count": 24, "percentage": 16.5, "label": "Needs Push" },
      "atRisk": { "count": 4, "percentage": 2.8, "label": "At Risk" }
    },
    "groupPerformanceComparison": [
      { "groupId": "...", "groupName": "Physics - Sec 2 (A)", "gradeLevel": "SecondaryTwo", "studentCount": 48, "averageScore": 84.2, "attendanceRate": 94.5, "sessionsCount": 8, "trend": "up" }
    ],
    "assessmentCategoryBreakdown": [
      { "category": "Exams", "averageScore": 79.5, "totalAssessments": 145 }
    ],
    "gradeLevelDistribution": [
      { "gradeLevel": "SecondaryTwo", "groupsCount": 3, "studentsCount": 90, "averageScore": 81.0 }
    ],
    "platformComparison": { "yourAverageScore": 78.4, "schoolAverageScore": 72.0, "yourRank": 3, "totalTeachers": 18, "performanceLabel": "Above Average" },
    "topStudents": { "items": [], "page": 1, "pageSize": 10, "totalCount": 0 },
    "recentUploads": []
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Caching: IMemoryCache, 2 minutes TTL. Cache key: dashboard:teacher:{userId}:{filterHash}.
Filters: groupId, gradeLevel, period, timeRange (7d, 30d, 90d, term, all, custom), dateFrom, dateTo, search, studentsPage, studentsPageSize (paginate the topStudents list; charts stay fixed).
Flutter Visuals: Formatted for LineChart, BarChart, Donut/Pie, Radar, and Gauge widgets.

Students Needing Attention

At-risk students (score < 50% or attendance < 60%) with failure reasons, missed sessions, and parent contact. Opened from the risk badge on Home / Dashboard.

Request / Response
Query Params
?groupId=...&gradeLevel=SecondaryTwo&reason=low_score&search=Omar&page=1&pageSize=20
Response 200 (PaginatedResult)
{
  "data": {
    "items": [
      { "studentId": "...", "fullName": "Omar Khaled", "studentCode": "STU-042", "groupId": "...", "groupName": "Physics - Sec 2 (A)", "gradeLevel": "SecondaryTwo", "reason": "Low score & Low attendance", "reasonCode": "low_score_low_attendance", "currentScore": 42.5, "attendanceRate": 0.4, "missedSessionsCount": 3, "parentName": "Khaled Hassan", "parentPhoneNumber": "+201012345678" }
    ],
    "page": 1, "pageSize": 20, "totalCount": 4
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Filters: groupId, gradeLevel, reason (low_score | low_attendance | low_score_low_attendance, case-insensitive — unknown values and display labels return 400), search; sorted worst-first (lowest score, then attendance).
Validation: page ≥ 1, pageSize 1–100 — violations return 400 ProblemDetails.
Flutter: render parentPhoneNumber as a tel: action button; lists arrive pre-sorted, don't re-sort client-side. Display reason (localized), but send back reasonCode as the filter — the display text is not a valid filter value.
👥 Groups 6 endpoints

Create Group

Define group name, grade level, subject, period, and optional weekly schedules.

Request / Response
Request Body
{
  "name": "Biology - Secondary 2",
  "academicYear": "2025-2026",
  "gradeLevel": "SecondaryTwo",
  "period": "First Term",
  "subjectId": "a1b2c3d4-5678-9abc-def0-1234567890ab",
  "schedules": [
    { "dayOfWeek": "Monday", "startTime": "14:00", "endTime": "16:00" },
    { "dayOfWeek": "Wednesday", "startTime": "14:00", "endTime": "16:00" }
  ]
}
Response 200
{
  "data": {
    "id": "c5d6e7f8-9abc-def0-1234-567890abcdef",
    "name": "Biology - Secondary 2",
    "academicYear": "2025-2026",
    "gradeLevel": "SecondaryTwo",
    "period": "First Term",
    "isActive": true,
    "teacherId": "b0000000-0000-0000-0000-000000000001",
    "subjectName": "Biology",
    "createdAt": "2026-09-12T10:00:00Z",
    "studentCount": 0, "averageScore": 0, "attendanceRate": 0, "activeSessions": 0,
    "schedules": [
      { "id": "...", "dayOfWeek": "Monday", "startTime": "14:00", "endTime": "16:00", "isActive": true }
    ]
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role. SubjectId must reference an existing Subject (GET /subjects to list, ?searchName= to filter by name).
Rate Limit: 30 req/min.
Enums:
· gradeLevel: 1=SecondaryOne, 2=SecondaryTwo, 3=SecondaryThree. Must be integer, not string.
· period: "First Term" | "Second Term". Restricted by CHECK constraint in DB.
· dayOfWeek (in schedules): "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday".
Validation: name (required, max 200 chars), academicYear (required, e.g. "2025-2026"), subjectId (required, must be valid GUID).
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (subject not found).

List My Groups

Paginated list of all your groups with stats.

Request / Response
Query Parameters
gradeLevel: string?  (filter by grade)
page: int = 1
pageSize: int = 20
Response 200
{
  "data": {
    "items": [
      {
        "id": "c5d6e7f8-...", "name": "Biology - Secondary 2",
        "academicYear": "2025-2026", "gradeLevel": "SecondaryTwo",
        "period": "First Term", "isActive": true,
        "subjectName": "Biology", "studentCount": 50,
        "averageScore": 88.5, "attendanceRate": 94.0,
        "activeSessions": 3, "lastUploadDate": "2026-09-12T14:30:00Z",
        "trend": "up",
        "schedules": [
          { "id": "...", "dayOfWeek": "Monday", "startTime": "14:00", "endTime": "16:00", "isActive": true }
        ]
      }
    ],
    "page": 1, "pageSize": 20, "totalCount": 5
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}

Get Group Detail

Single group with full details including schedules.

Request / Response
Response 200 — same shape as single item in list response above
Prerequisites: Logged in with Teacher. Group must belong to this teacher.
Rate Limit: 30 req/min.
Errors: 401 (no token), 403 (not your group), 404 (group not found).

Update Group

Update group name, grade and period. Subject is set at creation and cannot be changed. Schedules are managed separately.

Request / Response
Request Body
{
  "name": "Biology - Secondary 2 (Updated)",
  "academicYear": "2025-2026",
  "gradeLevel": "SecondaryTwo",
  "period": "Second Term"
}
Response 200 — same shape as GroupResponseDto

Delete Group

Soft-delete: sets IsActive = false. Preserves all data.

Request / Response
Response 200
{ "data": { "message": "Group deleted successfully" }, "meta": { "timestamp": "...", "requestId": "..." } }
Prerequisites: Logged in with Teacher. Group must belong to this teacher.
Rate Limit: 30 req/min.
Behavior: Soft-delete — sets IsActive = false. All sessions, students, and data are preserved. Group disappears from default list queries but can still be accessed by ID.
Errors: 401 (no token), 403 (not your group), 404 (group not found).

Duplicate Group

Clone a group with its schedules. Students are NOT copied.

Request / Response
Request Body
{ "name": "Biology - Secondary 2 (Copy)" }
Response 200
{
  "data": {
    "id": "...", "name": "Biology - Secondary 2 (Copy)",
    "academicYear": "2025-2026", "gradeLevel": "SecondaryTwo",
    "period": "First Term", "subjectId": "a1b2c3d4-..."
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Source group must belong to this teacher.
Rate Limit: 30 req/min.
Behavior: Clones group metadata + schedules. Does NOT copy students, sessions, or upload logs. New group is independent.
Errors: 400 (validation), 401 (no token), 403 (not your group), 404 (source group not found).
📅 Schedules 3 endpoints

Add Schedules to Group

Add weekly time slots to an existing group.

Request / Response
Request Body (array)
[
  { "dayOfWeek": "Tuesday", "startTime": "15:00", "endTime": "17:00" },
  { "dayOfWeek": "Thursday", "startTime": "15:00", "endTime": "17:00" }
]
Response 200
{
  "data": [
    { "id": "...", "dayOfWeek": "Tuesday", "startTime": "15:00", "endTime": "17:00", "isActive": true },
    { "id": "...", "dayOfWeek": "Thursday", "startTime": "15:00", "endTime": "17:00", "isActive": true }
  ],
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Group must belong to this teacher.
Rate Limit: 30 req/min.
Enums: dayOfWeek: "Monday"|"Tuesday"|"Wednesday"|"Thursday"|"Friday"|"Saturday"|"Sunday". startTime/endTime: "HH:mm" format (24h).
Errors: 400 (validation), 401 (no token), 403 (not your group), 404 (group not found).

Update Schedule

Change day or time of an existing schedule slot.

Request / Response
Request Body
{ "dayOfWeek": "Monday", "startTime": "16:00", "endTime": "18:00" }
Response 200 — empty object
Prerequisites: Logged in with Teacher. Schedule must belong to a group owned by this teacher.
Rate Limit: 30 req/min.
Enums: dayOfWeek is System.DayOfWeek serialized as its English name: Sunday…Saturday.
Errors: 400 (validation), 401 (no token), 403 (not your schedule), 404 (schedule not found).

Delete Schedule

Remove a weekly time slot from a group.

Request / Response
Response 200 — empty object
Prerequisites: Logged in with Teacher. Schedule must belong to a group owned by this teacher.
Rate Limit: 30 req/min.
Behavior: Permanently deletes the schedule slot. Cannot be undone.
Errors: 401 (no token), 403 (not your schedule), 404 (schedule not found).
🎓 Student Management 9 endpoints

List Students

Browse students visible to you. Filter by "my-students" or "available".

Request / Response
Query Parameters
search: string?     (search by name/email)
filter: string?     ("my-students" default = pending + active | "available")
page: int = 1
pageSize: int = 20 (max 100)
Response 200
{
  "data": {
    "items": [
      {
        "studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef",
        "fullName": "Sara Mohamed",
        "email": "sara@student.com",
        "studentCode": "STU-001",
        "isEnrolled": false,
        "enrolledGroups": [
          { "groupId": "c5d6e7f8-...", "groupName": "Biology - Secondary 2", "status": "Pending" }
        ],
        "stats": { "averageScore": 0, "attendanceRate": 0, "totalSessions": 0, "attendedSessions": 0, "assessmentCount": 0, "lastActivityDate": null, "scoreTrend": "stable", "pendingApprovalCount": 1 }
      }
    ],
    "page": 1, "pageSize": 20, "totalCount": 45
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Behavior: Defaults to my-students so the list never leaks platform-wide students. Pending memberships report zeroed metrics; they are not counted as absences.

Look Up Student by Code

Find a specific student using their unique student code.

Request / Response
Response 200 — minimal lookup (no email)
{
  "data": {
    "studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef",
    "fullName": "Sara Mohamed",
    "studentCode": "STU-001",
    "alreadyEnrolledWithTeacher": false
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Scoping: only enrolled students are visible to this teacher's lookup; alreadyEnrolledWithTeacher tells you whether the code already belongs to one of your groups.
Privacy: email and contact data are deliberately excluded.
Errors: 401 (no token), 404 (unknown code).

Enroll Student by ID

Add a student to a group by their GUID.

Request / Response
Request Body
{ "studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef" }
Response 200
{
  "data": {
    "studentId": "d4e5f6a7-...",
    "fullName": "Sara Mohamed",
    "email": "sara@student.com",
    "studentCode": "STU-001",
    "isEnrolled": false,
    "enrolledGroups": [{ "groupId": "c5d6e7f8-...", "groupName": "Biology - Secondary 2", "status": "Pending" }],
    "stats": { "averageScore": 0, "attendanceRate": 0, "totalSessions": 0, "attendedSessions": 0, "assessmentCount": 0, "lastActivityDate": null, "scoreTrend": "stable", "pendingApprovalCount": 1 }
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Group must belong to this teacher. Student must exist and not already be enrolled in this group.
Rate Limit: 30 req/min.
Behavior: Creates a Pending membership (StudentsGroups.Status = Pending). The student is excluded from rosters, sessions, templates, and uploads until approved. studentCode is generated once if missing and never overwritten.
Errors: 400 (validation), 401 (no token), 403 (not your group), 404 (student not found), 409 (already enrolled).

Enroll Student by Code

Add a student to a group using their student code. No request body needed.

Request / Response
Query: groupId={groupId} (required)
Response 200 — same StudentResponseDto shape, status: "Pending"
Behavior: Identical lifecycle to enroll-by-ID: the membership starts as Pending and must be approved before it appears in rosters or sessions.

Get Student Progress

Detailed progress: attendance, scores, per-group breakdown — scoped to your groups.

Request / Response
Response 200
{
  "data": {
    "studentId": "d4e5f6a7-...",
    "fullName": "Sara Mohamed",
    "overallAttendancePercentage": 92.0,
    "overallAverageWrittenScore": 85.5,
    "overallAverageAssignmentScore": 88.0,
    "overallProgressPercentage": 87.0,
    "totalSessionsAcrossAllGroups": 25,
    "perGroupProgress": [
      {
        "studentId": "d4e5f6a7-...",
        "studentName": "Sara Mohamed",
        "studentCode": "STU-001",
        "groupId": "c5d6e7f8-...",
        "groupName": "Biology - Secondary 2",
        "enrollmentStatus": "Active",
        "totalSessions": 12,
        "attendedSessions": 11,
        "attendancePercentage": 91.7,
        "averageWrittenScore": 86.0,
        "averageAssignmentScore": 89.0,
        "overallProgressPercentage": 87.5,
        "recentAssessments": [
          {
            "assessmentId": "e5f6a7b8-...",
            "examName": "Unit 3 Written",
            "topic": "Mitosis",
            "kind": "Written",
            "rawScore": 18,
            "maxScore": 20,
            "scorePercentage": 90,
            "teacherComment": "Excellent",
            "sessionDate": "2026-09-12T14:00:00"
          }
        ]
      }
    ]
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Scoping: only groups owned by the calling teacher are included. No shared group → 403.
Metrics: attendance counts recorded attendance sessions inside the membership window; a missing row counts as absent.

Get Student's Parent Contact

Retrieve parent name, email, and phone for a student.

Request / Response
Response 200
{
  "data": {
    "parentId": "f1a2b3c4-...",
    "fullName": "Mohamed Ali",
    "email": "mohamed@parent.com",
    "phoneNumber": "+201055512345"
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Authorization: requires a shared group between the teacher and the student; otherwise 403. Contact data is never returned for students outside the teacher's groups.

Unenroll Student

Remove a student from a group. Preserves historical data.

Request / Response
Response 200
{ "data": { "message": "Student unenrolled successfully" }, "meta": { "timestamp": "...", "requestId": "..." } }
Prerequisites: Logged in with Teacher. Student must be pending or active in this group.
Rate Limit: 30 req/min.
Behavior: Soft-unenroll — the membership row becomes Removed with removedAt set. Historical sessions and grades remain readable by the student and parent; the account itself is untouched. Re-enrolling later reactivates the same row as Pending and keeps the existing student code.
Errors: 401 (no token), 403 (not your group), 404 (student not in group).

Approve Group Enrollment

Activate a pending membership in a specific group.

Request / Response
Response 200
{ "data": { "message": "Student approved successfully" }, "meta": { "timestamp": "...", "requestId": "..." } }
Prerequisites: Logged in with Teacher. Group must belong to this teacher and contain a Pending membership for the student.
Rate Limit: 30 req/min.
Behavior: Sets the membership Status = Active and stamps approvedAt. Approval is group-scoped and does not touch User.IsActive. Approving an already-active membership is idempotent.
Errors: 401 (no token), 403 (not your group), 404 (membership not found).

Export Students as Excel

Download a spreadsheet of all students in a group with their stats.

Request / Response
Response 200 — binary .xlsx file
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="students_c5d6e7f8.xlsx"
📅 Sessions 7 endpoints

List All Sessions

All sessions across your groups, paginated.

Request / Response
Response 200
{
  "data": {
    "items": [
      {
        "sessionId": "e7f8a9b0-...",
        "studentId": "d4e5f6a7-...",
        "studentName": "Sara Mohamed",
        "studentCode": "STU-001",
        "groupName": "Biology - Secondary 2",
        "sessionDate": "2026-09-12T14:00:00",
        "sessionType": "Quiz",
        "attended": true,
        "score": 45,
        "maxScore": 50,
        "scorePercentage": 90.0,
        "comment": null
      }
    ],
    "page": 1, "pageSize": 20, "totalCount": 156
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Query: page (default 1), pageSize (default 20, max 100).
Behavior: One row per student per session across all groups owned by this teacher (not one row per session). Sorted by session date descending, then student name. Teacher rows carry studentId/studentName/studentCode; group rows by sessionId client-side.
Errors: 401 (no token), 403 (wrong role).

Create Session

Create a session with optional Excel header definitions. Headers define which columns your upload Excel will have.

Request / Response
Request Body
{
  "sessionDate": "2026-09-12T14:00:00",
  "sessionType": "Quiz",
  "topic": "Cell Division - Mitosis",
  "description": "Chapter 3 review",
  "headers": [
    { "key": "attendance", "label": "Attendance", "labelArabic": "الحضور", "columnType": "Attendance", "sortOrder": 0, "isRequired": true },
    { "key": "written_exam", "label": "Written Exam", "labelArabic": "الامتحان الكتابي", "columnType": "Score", "maxScore": 50, "sortOrder": 1, "isRequired": true },
    { "key": "oral_quiz", "label": "Oral Quiz", "labelArabic": "الامتحان الشفهي", "columnType": "Score", "maxScore": 20, "sortOrder": 2, "isRequired": false },
    { "key": "comment", "label": "Comment", "labelArabic": "ملاحظة", "columnType": "Comment", "sortOrder": 3, "isRequired": false }
  ]
}
Response 201
{
  "data": {
    "id": "e7f8a9b0-1234-5678-9abc-def012345678",
    "sessionDate": "2026-09-12T14:00:00",
    "sessionType": "Quiz",
    "topic": "Cell Division - Mitosis",
    "description": "Chapter 3 review",
    "createdAt": "2026-09-12T10:30:00Z",
    "headers": [
      { "id": "...", "key": "attendance", "label": "Attendance", "labelArabic": "الحضور", "columnType": "Attendance", "maxScore": null, "sortOrder": 0, "isRequired": true },
      { "id": "...", "key": "written_exam", "label": "Written Exam", "labelArabic": "الامتحان الكتابي", "columnType": "Score", "maxScore": 50, "sortOrder": 1, "isRequired": true },
      { "id": "...", "key": "oral_quiz", "label": "Oral Quiz", "labelArabic": "الامتحان الشفهي", "columnType": "Score", "maxScore": 20, "sortOrder": 2, "isRequired": false },
      { "id": "...", "key": "comment", "label": "Comment", "labelArabic": "ملاحظة", "columnType": "Comment", "maxScore": null, "sortOrder": 3, "isRequired": false }
    ]
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role. Group must exist and belong to this teacher.
Rate Limit: 30 req/min.
Enums:
· sessionType: 0=Exam, 1=Quiz, 2=Homework, 3=Oral, 4=Attendance. Integer, not string.
· columnType (in headers): 0=Attendance, 1=Score, 2=Comment. Integer, not string.
Validation: sessionDate (required), topic (required), description (optional). Headers array items: key (required, unique per session), label (required), columnType (required: Attendance | Score | Comment — strings), sortOrder (required, 0-based), isRequired (default false). maxScore required when columnType is Score, and must be 1-1000.
Behavior: If headers omitted, defaults to Attendance + Written 60 + Assignment 20 + Comment. Use copyHeadersFromSessionId to reuse headers from a previous session (headers param is ignored if this is set).
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (group not found).

Get Session Detail

Full session with all student sessions and assessment breakdowns.

Request / Response
Response 200
{
  "data": {
    "sessionId": "e7f8a9b0-...",
    "sessionDate": "2026-09-12T14:00:00",
    "sessionType": "Quiz",
    "topic": "Cell Division - Mitosis",
    "description": "Chapter 3 review",
    "groupName": "Biology - Secondary 2",
    "subjectName": "Biology",
    "teacherName": "Ahmed Hassan",
    "studentSessions": [
      {
        "studentSessionId": "...",
        "studentId": "d4e5f6a7-...",
        "studentName": "Sara Mohamed",
        "studentCode": "STU-001",
        "attended": true,
        "assessments": [
          { "assessmentId": "...", "examName": "written_exam", "topic": "Mitosis", "kind": "Written", "rawScore": 45, "maxScore": 50, "scorePercentage": 90, "teacherComment": "Good work" },
          { "assessmentId": "...", "examName": "oral_quiz", "topic": "Mitosis", "kind": "Oral", "rawScore": 18, "maxScore": 20, "scorePercentage": 90, "teacherComment": "Excellent" }
        ]
      }
    ]
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Session must belong to this teacher.
Rate Limit: 30 req/min.
Response: Full session with all StudentSessions and their Assessments. Includes group name, subject name, teacher name.
Errors: 401 (no token), 403 (not your session), 404 (session not found).

Update Session

Change session date, topic, description, or type.

Request / Response
Request Body (all fields optional)
{
  "sessionDate": "2026-09-13T14:00:00",
  "topic": "Cell Division - Meiosis",
  "description": "Updated topic",
  "sessionType": "Exam"
}
Response 200
{
  "data": {
    "id": "e7f8a9b0-...",
    "sessionDate": "2026-09-13T14:00:00",
    "sessionType": "Exam",
    "topic": "Cell Division - Meiosis",
    "description": "Updated topic",
    "createdAt": "2026-09-12T10:30:00Z"
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Session must belong to this teacher.
Rate Limit: 30 req/min.
Validation: All fields optional. Only provided fields are updated (partial update). sessionType uses same enum as create (0-4).
Errors: 400 (validation), 401 (no token), 403 (not your session), 404 (session not found).

Delete Session

Permanently delete a session and all its assessments.

Request / Response
Response 204 — No Content

Mark Attendance

Bulk mark attendance for all students in a session.

Request / Response
Request Body
{
  "attendances": [
    { "studentId": "d4e5f6a7-...", "attended": true },
    { "studentId": "a1b2c3d4-...", "attended": false },
    { "studentId": "e5f6a7b8-...", "attended": true }
  ]
}
Response 200 — empty object
Prerequisites: Logged in with Teacher. Session must belong to this teacher. Students must be enrolled in the session's group.
Rate Limit: 30 req/min.
Behavior: Bulk operation. Creates or updates StudentSession records with the attended flag. Idempotent — calling again overwrites previous attendance.
Errors: 400 (validation), 401 (no token), 403 (not your session), 404 (session/student not found).
📋 Session Headers 2 endpoints

Get Session Headers

View the Excel column configuration for a session.

Request / Response
Response 200
{
  "data": [
    { "id": "...", "key": "attendance", "label": "Attendance", "labelArabic": "الحضور", "columnType": "Attendance", "maxScore": null, "sortOrder": 0, "isRequired": true },
    { "id": "...", "key": "written_exam", "label": "Written Exam", "labelArabic": "الامتحان الكتابي", "columnType": "Score", "maxScore": 50, "sortOrder": 1, "isRequired": true },
    { "id": "...", "key": "oral_quiz", "label": "Oral Quiz", "labelArabic": "الامتحان الشفهي", "columnType": "Score", "maxScore": 20, "sortOrder": 2, "isRequired": false },
    { "id": "...", "key": "comment", "label": "Comment", "labelArabic": "ملاحظة", "columnType": "Comment", "maxScore": null, "sortOrder": 3, "isRequired": false }
  ],
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Session must belong to this teacher.
Rate Limit: 30 req/min.
Response: Array of SessionHeaderDefinitionDto (not wrapped in pagination). Sorted by sortOrder ascending.
Errors: 401 (no token), 403 (not your session), 404 (session not found).

Update Session Headers

Change the Excel column configuration. Locked (409) once scores are uploaded.

Request / Response
Request Body
{
  "headers": [
    { "key": "attendance", "label": "Attendance", "columnType": "Attendance", "sortOrder": 0, "isRequired": true },
    { "key": "written_exam", "label": "Written Exam", "columnType": "Score", "maxScore": 50, "sortOrder": 1, "isRequired": true },
    { "key": "bonus", "label": "Bonus Points", "columnType": "Score", "maxScore": 10, "sortOrder": 2, "isRequired": false }
  ],
  "copyHeadersFromSessionId": null
}
Response 200
{
  "data": {
    "sessionId": "e7f8a9b0-...",
    "headers": [
      { "id": "...", "key": "attendance", "label": "Attendance", "columnType": "Attendance", "maxScore": null, "sortOrder": 0, "isRequired": true },
      { "id": "...", "key": "written_exam", "label": "Written Exam", "columnType": "Score", "maxScore": 50, "sortOrder": 1, "isRequired": true },
      { "id": "...", "key": "bonus", "label": "Bonus Points", "columnType": "Score", "maxScore": 10, "sortOrder": 2, "isRequired": false }
    ]
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role. Session must exist and belong to this teacher. Must NOT have any uploaded scores (StudentSessions with Assessments).
Rate Limit: 30 req/min.
Lock Condition: Returns 409 Conflict if scores already exist for this session. Delete existing scores first or create a new session.
Alternative: Use copyHeadersFromSessionId in the request body to copy headers from another session instead of defining them manually.
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (session not found), 409 (scores exist — headers locked).
📈 Assessments 3 endpoints

Add Assessment

Add a score record for a student in a session. Creates or updates the assessment.

Request / Response
Request Body
{
  "studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef",
  "examName": "written_exam",
  "rawScore": 45,
  "maxScore": 50,
  "kind": "Written",
  "teacherComment": "Good understanding of mitosis phases"
}
Response 201
{
  "data": {
    "id": "...",
    "examName": "written_exam",
    "topic": null,
    "kind": "Written",
    "rawScore": 45,
    "maxScore": 50,
    "scorePercentage": 90,
    "teacherComment": "Good understanding of mitosis phases"
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role. Session must exist. Student must be enrolled in the session's group. A StudentSession record must exist for this student+session combo.
Rate Limit: 30 req/min.
Validation: studentId (required, valid GUID), examName (required, must match a header key), scorePercentage (required, 0-100), maxScore (required, > 0).
Behavior: scorePercentage is stored as percentage (0-100), not raw score. The system calculates: rawScore = (scorePercentage / 100) * maxScore. Creates a new Assessment record linked to the StudentSession.
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (session/student not found).

Update Assessment

Change score or comment on an existing assessment.

Request / Response
Request Body (both optional)
{ "scorePercentage": 48, "teacherComment": "Updated: excellent progress" }
Response 200
{
  "data": {
    "id": "...", "examName": "written_exam", "topic": "",
    "scorePercentage": 48, "teacherComment": "Updated: excellent progress"
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Assessment must exist and belong to a session owned by this teacher.
Rate Limit: 30 req/min.
Validation: Both fields optional (partial update). scorePercentage: 0-100.
Errors: 400 (validation), 401 (no token), 403 (not your assessment), 404 (assessment not found).

Get Average Score

Quick summary: average, min, max scores and attendance for a session.

Request / Response
Response 200
{
  "data": {
    "sessionId": "e7f8a9b0-...",
    "sessionType": "Quiz",
    "sessionDate": "2026-09-12T14:00:00",
    "groupName": "Biology - Secondary 2",
    "totalStudents": 50,
    "attendedStudents": 46,
    "attendanceRate": 92.0,
    "averageScore": 82.8,
    "minScore": 35,
    "maxScore": 100,
    "totalAssessments": 138
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Session must belong to this teacher with uploaded scores.
Rate Limit: 30 req/min.
Response: Lightweight summary — faster than /report. TotalAssessments = total assessment records (one per score column per student).
Errors: 401 (no token), 403 (not your session), 404 (session not found).
📎 Excel Upload 5 endpoints

Download Template

Get an Excel template pre-filled with enrolled student names/codes AND your custom header columns.

Request / Response
Query Parameters
groupId: uuid?    (legacy mode: populates student names)
sessionId: uuid?  (session-bound mode: headers match session config)
Response 200 — binary .xlsx file
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="template_20260912.xlsx"
Prerequisites: Logged in with Teacher role. If sessionId provided, session must exist with configured headers. If groupId provided, group must have enrolled students.
Rate Limit: 5 req/min (stricter — file generation is expensive).
Modes:
· Session-bound (recommended): Pass sessionId. Template columns match session headers exactly. Student names/codes auto-filled from enrolled students.
· Legacy: Pass groupId only. Uses default headers (Attendance, Written 60, Assignment 20, Comment).
Errors: 401 (no token), 403 (wrong role), 404 (session/group not found).

Upload Excel

Fill the template with attendance + scores, then upload. Hangfire processes it async. Strategy: replace (overwrite), skip (keep existing), or error (reject if scores exist).

Request / Response
multipart/form-data
file: (binary .xlsx, max 10MB)
groupId: c5d6e7f8-9abc-def0-1234-567890abcdef
sessionId: e7f8a9b0-1234-5678-9abc-def012345678  (optional)
assessmentDate: 2026-09-12T14:00:00  (optional)
strategy: replace  (error | replace | skip)
Response 200
{
  "data": {
    "uploadId": "f9a0b1c2-3456-789a-bcde-f01234567890",
    "fileName": "template_20260912.xlsx",
    "status": "Queued",
    "uploadedAt": "2026-09-12T14:30:00Z"
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role. File must be .xlsx, .xls, or .csv (max 10MB). Group must exist. If sessionId provided, session must have headers configured.
Rate Limit: 5 req/min (stricter — file upload is expensive).
Enums:
· strategy: "error" | "replace" | "skip". Query parameter, not body.
  - "error": Returns 409 if scores already exist for this session.
  - "replace": Deletes all existing StudentSessions + Assessments, creates fresh from Excel.
  - "skip": Keeps students who already have scores, only adds new students.
Behavior: File is saved to disk at uploads/excel/{yyyy}/{MM}/{dd}/{guid}_{filename}. Processing is async via Hangfire (ProcessExcelJob on "uploads" queue). Always returns status="Queued". Poll GET /upload-logs to check completion.
Errors: 400 (invalid file), 401 (no token), 403 (wrong role), 409 (strategy=error and scores exist), 413 (file too large).

List Upload Logs

Paginated upload history with status, row counts, and group info.

Request / Response
Query Parameters
uploadStatuses: UploadStatus[]?  (repeatable: 0=Queued, 1=Processing, 2=Completed, 3=Failed; omit for all)
page: int = 1
pageSize: int = 20 (max 100)
Response 200
{
  "data": {
    "items": [
      {
        "uploadId": "f9a0b1c2-...",
        "fileName": "template_20260912.xlsx",
        "fileSize": 24576,
        "status": "Completed",
        "uploadedAt": "2026-09-12T14:30:00Z",
        "completedAt": "2026-09-12T14:30:15Z",
        "rawProcessed": 50,
        "rawFailed": 0,
        "groupId": "c5d6e7f8-...",
        "groupName": "Biology - Secondary 2"
      }
    ],
    "page": 1, "pageSize": 10, "totalCount": 8
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Enums:
· status: Queued, Processing, Completed, Failed (serialized as strings).
Filters: repeatable ?uploadStatuses= (e.g. Queued, Failed); omit or empty for all. Filter applies before totalCount, so counts match the filtered list.
Polling: After upload, poll this endpoint every 2-3 seconds. Check if status left Queued/Processing.
Errors: 401 (no token), 403 (wrong role).

Get Upload Log Detail

Single upload log with full details.

Request / Response
Response 200 — same shape as single item in list

Retry Failed Upload

Re-queue a failed upload for processing.

Request / Response
Response 200 — empty object
Prerequisites: Logged in with Teacher role. Upload log must exist with status=3 (Failed). The uploaded file must still exist on disk (not cleaned up by CleanupOldFilesJob).
Rate Limit: 30 req/min.
Behavior: Re-queues the same file for processing with the same strategy. Creates a new Hangfire job. The upload status resets to Queued (0).
Errors: 401 (no token), 403 (wrong role), 404 (upload log not found), 409 (status is not Failed).
📊 Reports & Export 3 endpoints

Session Report

Professional report: column breakdown, highlights, chart series, needs-attention, comparison with previous session. Cached 1 minute.

Request / Response
Response 200 (key fields)
{
  "data": {
    "session": {
      "sessionId": "e7f8a9b0-...", "sessionDate": "2026-09-12T14:00:00",
      "sessionType": "Quiz", "topic": "Cell Division - Mitosis",
      "groupName": "Biology - Secondary 2", "subjectName": "Biology"
    },
    "analytics": {
      "totalStudents": 50, "attendedStudents": 46, "attendanceRate": 92.0,
      "averageScore": 82.8, "minScore": 35, "maxScore": 100, "medianScore": 85.0,
      "passCount": 42, "failCount": 4, "passRate": 87.5,
      "gradeDistribution": { "A": 31, "B": 33, "C": 21, "D": 7 }
    },
    "columnBreakdown": [
      { "key": "written_exam", "label": "Written Exam", "maxScore": 50, "average": 38.5, "min": 15, "max": 50, "passRate": 90.0, "assessedCount": 46 },
      { "key": "oral_quiz", "label": "Oral Quiz", "maxScore": 20, "average": 16.2, "min": 8, "max": 20, "passRate": 85.0, "assessedCount": 46 }
    ],
    "highlights": {
      "topStudents": [{ "studentId": "...", "studentName": "Ali Hassan", "studentCode": "STU-003", "average": 98.5, "delta": 3.2 }],
      "atRisk": [{ "studentId": "...", "studentName": "Omar Ali", "studentCode": "STU-012", "average": 35.0, "delta": -5.0 }],
      "mostImproved": [{ "studentId": "...", "studentName": "Fatma SA", "studentCode": "STU-008", "average": 88.0, "delta": 12.5 }],
      "biggestDrop": []
    },
    "chartSeries": {
      "histogram": [{ "bucket": "90-100", "count": 18 }, { "bucket": "80-89", "count": 15 }, { "bucket": "70-79", "count": 8 }, { "bucket": "60-69", "count": 5 }, { "bucket": "0-59", "count": 4 }],
      "byColumn": [{ "label": "Written Exam", "average": 82.8 }, { "label": "Oral Quiz", "average": 81.0 }]
    },
    "needsAttention": [
      { "type": "low_attendance", "message": "4 students absent", "count": 4 },
      { "type": "failing_scores", "message": "4 students below 60%", "count": 4 }
    ],
    "comparison": {
      "previousAvgScore": 78.2, "currentAvgScore": 82.8, "scoreChange": 4.6,
      "previousAttendanceRate": 88.0, "currentAttendanceRate": 92.0, "attendanceChange": 4.0,
      "perColumnDelta": [
        { "key": "written_exam", "label": "Written Exam", "previousAverage": 35.0, "currentAverage": 38.5, "delta": 3.5 }
      ]
    }
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role. Session must exist with uploaded scores (at least one StudentSession + Assessment).
Rate Limit: 30 req/min.
Caching: IMemoryCache, 1 minute TTL. Cache key: session_report_{sessionId}. Data is stale for up to 60s after new uploads.
Response Fields: session (info), analytics (11 stats + gradeDistribution), columnBreakdown[] (per-column avg/min/max/passRate), highlights (topStudents/atRisk/mostImproved/biggestDrop — max 5 each), chartSeries (histogram 5 buckets + byColumn), needsAttention[] (max 5 items), comparison (nullable — null if no previous session for this group).
GradeDistribution Keys: "A" (90-100), "B" (80-89), "C" (70-79), "D" (60-69), "F" (below 60). Based on average score percentage.
Errors: 401 (no token), 403 (wrong role), 404 (session not found).

Export Session Report

Download session report as an Excel file.

Request / Response
Response 200 — binary .xlsx file
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="report_e7f8a9b0.xlsx"
Prerequisites: Logged in with Teacher. Session must belong to this teacher with uploaded scores.
Rate Limit: 30 req/min.
Behavior: Generates Excel with attendance, scores per column, grade distribution. File is generated on-the-fly (not cached).
Errors: 401 (no token), 403 (not your session), 404 (session not found).

Export Students

Download all students in a group as Excel with their stats.

Request / Response
Response 200 — binary .xlsx file
Prerequisites: Logged in with Teacher. Group must belong to this teacher.
Rate Limit: 30 req/min.
Behavior: Generates Excel with student names, codes, enrollment dates, attendance rates, average scores. One row per student.
Errors: 401 (no token), 403 (not your group), 404 (group not found).
🏆 Leaderboard 2 endpoints

Group Leaderboard

Rankings for a specific group. Recomputed hourly by Hangfire.

Request / Response
Query: period = daily | weekly | monthly | alltime
Response 200
{
  "data": [
    {
      "rank": 1, "studentId": "d4e5f6a7-...",
      "studentName": "Ali Hassan", "studentCode": "STU-003",
      "weightedScore": 95.5, "avgScore": 92.0, "attendanceRate": 100.0, "participationScore": 10.0,
      "context": {
        "totalStudents": 50, "scoreDifferenceToTop": 0,
        "averageScore": 82.8, "topScore": 95.5,
        "isTopPerformer": true, "rankLabel": "#1"
      }
    },
    {
      "rank": 2, "studentId": "...",
      "studentName": "Sara Mohamed", "studentCode": "STU-001",
      "weightedScore": 91.2, "avgScore": 88.0, "attendanceRate": 95.0, "participationScore": 9.0,
      "context": { "totalStudents": 50, "scoreDifferenceToTop": 4.3, "averageScore": 82.8, "topScore": 95.5, "isTopPerformer": false, "rankLabel": "#2" }
    }
  ],
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher. Group must belong to this teacher. Leaderboard must have been computed (hourly Hangfire job).
Rate Limit: 30 req/min.
Enums:
· period: "daily" | "weekly" | "monthly" | "alltime". Default: "weekly".
Response: Array (not paginated). Sorted by rank ascending. WeightedScore = avgScore * 0.6 + attendanceRate * 0.3 + participationScore * 0.1.
Errors: 401 (no token), 403 (not your group), 404 (group not found).

Teacher-Wide Leaderboard

Combined rankings across all your groups. Paginated.

Request / Response
Query: period, gradeLevel?, page, pageSize
Response 200 — same PaginatedResult<LeaderboardEntryDto> shape
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Query: period (default "alltime"), gradeLevel (optional filter), page (default 1), pageSize (default 20, max 100).
Behavior: Combines rankings across ALL groups owned by this teacher. A student appears once with their best score across groups.
Errors: 401 (no token), 403 (wrong role).
🔔 Notifications 6 endpoints

List Notifications

Your notifications: badge awards, payment confirmations, system alerts.

Request / Response
Response 200
{
  "data": {
    "items": [
      {
        "id": "...", "userId": "b0000000-...",
        "title": "Payment Confirmed", "body": "Sara Mohamed's payment of 1500 EGP confirmed",
        "type": "PaymentConfirmed", "isRead": false,
        "payload": null, "sentAt": "2026-09-12T10:00:00Z"
      }
    ],
    "page": 1, "pageSize": 20, "totalCount": 12
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role.
Rate Limit: 30 req/min.
Query: page (default 1), pageSize (default 20, max 100).
Response Fields: id, userId, title, body, type (string like "PaymentConfirmed"), isRead (bool), payload (nullable JSON string), sentAt (nullable DateTime).
Errors: 401 (no token), 403 (wrong role).

Unread Count

Quick badge count for the notification bell.

Request / Response
Response 200
{ "data": 3, "meta": { "timestamp": "...", "requestId": "..." } }

Mark Read / Read All

Mark a single notification as read, or mark all as read.

Request / Response
Both return empty object on success
{ "data": {}, "meta": { "timestamp": "...", "requestId": "..." } }
Prerequisites: Logged in with Teacher role. For mark-read: notification must belong to this user.
Rate Limit: 30 req/min.
Behavior: mark-read sets isRead=true on one notification. read-all sets isRead=true on ALL unread notifications for this user.
Errors: 401 (no token), 403 (wrong role), 404 (notification not found for mark-read).

Send Notification to Parent

Send a notification to a specific student's parent.

Request / Response
Request Body
{
  "studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef",
  "title": "Exam Results",
  "body": "Sara scored 90% in the Cell Division quiz",
  "type": "NewGrade"
}
Response 201 — No body
Prerequisites: Logged in with Teacher role. Student must exist and have a linked parent (Student.ParentId != null).
Rate Limit: 30 req/min.
Enums:
· type: NewGrade, RankChange, BadgeEarned, PaymentConfirmed, SystemAlert, LeaderboardResult (serialized as strings).
Behavior: Creates a single Notification record for the student's parent. Bilingual EN/AR — title and body are stored as-is (caller provides translated version if needed).
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (student not found or student has no parent).

Bulk Notify Parents

Send a notification to all parents in a group. Returns count of notifications sent.

Request / Response
Request Body
{
  "title": "Exam Results Available",
  "body": "The results for the Cell Division quiz are now available. Class average: 82.8%",
  "type": "SystemAlert"
}
Response 200
{ "data": 15, "meta": { "timestamp": "...", "requestId": "..." } }
Prerequisites: Logged in with Teacher role. Group must exist and belong to this teacher. Students in the group must have linked parents.
Rate Limit: 30 req/min.
Enums:
· type: strings — NewGrade, RankChange, BadgeEarned, PaymentConfirmed, SystemAlert, LeaderboardResult.
Behavior: Creates one Notification per parent (not per student). If a parent has multiple children in the group, they still get only one notification. Response data = count of notifications sent.
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (group not found).
📚 Subjects 1 endpoint

Look Up Subject by Name

Find a subject's ID by its English or Arabic name. Useful for creating groups.

Request / Response
Response 200
{
  "data": {
    "id": "a1b2c3d4-5678-9abc-def0-1234567890ab",
    "name": "Biology",
    "nameArabic": "أحياء",
    "isActive": true,
    "createdAt": "2026-01-01T00:00:00Z",
    "updatedAt": null
  },
  "meta": { "timestamp": "...", "requestId": "..." }
}
Prerequisites: Logged in with Teacher role. Subject must exist.
Rate Limit: 30 req/min.
Behavior: Case-insensitive name search. Use the returned id as subjectId when creating groups (POST /teachers/groups).
Related Endpoints: GET /subjects (list all, paginated, ?searchName= filter), POST /subjects (admin only, create new).
Errors: 401 (no token), 403 (wrong role), 404 (subject not found by name).
⚙
Session Excel Lifecycle (NEW)
End-to-end flow: Teacher creates a session with custom Excel columns, fills scores via Excel upload, system processes and generates a report.
Create Session POST /groups/{id}/sessions Configure Headers PUT /sessions/{id}/headers Written Exam (Score, max=50) Download Template GET /upload/template Students + custom columns Upload Excel POST /upload?strategy= Hangfire Job ProcessExcelJob ParseSession + Process StudentSessions Created + Assessments per column Attended, scores, comments Report Generated GET /report Option B analytics Hourly Job Leaderboard Computation Leaderboard Rank + Score per student Daily Job Badge Awarding 4 Badge Types Week/Attendance/Top/Improver Upload Strategies error Reject if scores exist (409) replace Delete old, create fresh skip Keep scored students, add new All create new StudentSession IDs Cache Strategy Report: 1 min TTL Dashboard: 2 min TTL
▶
Registration → Scores → Leaderboard
Register POST /register Verify Enroll StudentsGroups Template GET /template Upload Excel Hangfire Job ProcessExcel Session + Assessments Hourly Job Leaderboard Leaderboard Updated Daily Job BadgeAwarding 4 Badge Types Notification
◆
Payment Lifecycle
Parent Creates Pending POST /payments ? Teacher Reviews PUT /confirm Confirmed Disputed
⚙
File Lifecycle
Upload Excel Save to disk uploads/{yyyy}/{MM}/{dd}/ ProcessExcelJob EPPlus UploadLog OK CleanupOldFilesJob > 30 days
★
Badge Awarding Pipeline
4 Auto-Awarded Badge Types (catalog has 7)
StudentOfWeek — Rank 1 in weekly leaderboard
PerfectAttendance — 100% attendance for month
HighestScore — Highest score in a session
Consistency — Score improved 3+ sessions
Catalog also defines StudentOfMonth, MostImproved, TopicMaster; the daily job does not award them.
Evaluation Flow
1. BadgeAwardingJob runs daily via Hangfire
2. Evaluates each badge type per student
3. Idempotent: checks if already awarded in period
4. Creates StudentsBadges record + notification
✉
Notification System
Teacher Sends POST /send Notification Created Bilingual EN/AR Parent Receives Batch Notify POST /groups/{id}/notify-parents Creates N notifications (one per parent)
⚡
JWT Authentication Lifecycle
Login Tokens Issued Access: 15min Refresh: 7 days Access Token Bearer + JWT API Calls Validated Expired POST /refresh-token → new pair 401 → Login again
⚙
Hangfire Job Pipeline
Hangfire uploads (5w) scheduled (5w) default (5w) ProcessExcel Leaderboard BadgeAwarding SQL Server Hangfire state Dashboard /hangfire Retry 3x exp. backoff
⚠
Known Issues
CriticalSession Upload Concurrency Bug (FIXED)
DbUpdateConcurrencyException on replace strategy. Root cause: tracker pollution from GetByIdWithHeadersAsync loading StudentSessions+Assessments. Fixed: ChangeTracker.Clear() before delete, existingCount-based condition, re-load entities after delete.
CriticalUploadLog Not Updated After Replace (FIXED)
ChangeTracker.Clear() inside replace block detached uploadLog entity. Final SaveChangesAsync had nothing to commit. Fixed: re-upload, re-session, re-headers after replace block.
MediumInvalid Login Returns 400 Instead of 401
POST /auth/login returns 400 for bad credentials. Clients cannot distinguish validation errors from auth failures.
MediumMissing Resource Returns 400 Instead of 404
GET /teachers/groups/{unknown-id} returns 400. Clients cannot distinguish validation from not-found.
MediumGradeLevel Enum Starts at 1, Docs Say 0
SecondaryOne=1, but validation rejects 0. Documentation and behavior disagree on valid range.
LowPhoneNumber Required on Profile Updates
PUT /profile for all 3 roles requires PhoneNumber though docs mark it optional.
LowTest Users 401 in Production
Seeded credentials do not work on live server. Only dev quick-login works.
LowGET /subjects Requires Auth
GET /subjects is reference data but returns 401 without a token.
LowArabic Text Encoding Garbled
nameArabic values come back corrupted — DB collation or response encoding issue.
✓
Student Module Contracts (2026-10) — breaking, fixed
BreakingEnrollment is now a lifecycle on StudentsGroups
Pending → Active → Removed with ApprovedAt/RemovedAt. Approval moved to PUT /teachers/groups/{groupId}/students/{studentId}/approve; unenroll is a soft delete that keeps history readable; StudentCode is generated once and never overwritten.
FixedAttendance and score metrics were computed in memory
Attendance, averages, prominence, trend, roster, and leaderboard figures now aggregate in SQL. Eligible attendance session = SessionType.Attendance inside the membership window with at least one recorded row; a missing student row counts as absent; sessions with no attendance data are excluded; pending memberships never contribute absences.
FixedScores lost their raw value
Assessment.RawScore and AssessmentKind added; every score response now exposes rawScore, maxScore, scorePercentage. Raw wins when supplied; percentage-only input derives the raw score via ScoreMath.
FixedLeaderboard attendance unit mismatch
Storage keeps a 0–1 fraction; every endpoint now reports 0–100 through the shared LeaderboardEntryMapper.
FixedBadge list was paginated over a fixed catalog
GET /students/badges returns the whole catalog with isEarned/awardedAt; paging params and the PaginatedResult envelope are gone, and metadata is now metaData.
VerifiedLINQ ↔ T-SQL translation proven on real SQL Server
Opt-in suite (T2I_SQLSERVER_TESTS=1) creates a throw-away LocalDB database, applies every migration, and exercises each student-module repository. Migration 20261027084034_StudentModuleContracts backfills memberships and assessments.
◆
Architecture Gaps
MediumCI/CD runs only on main
.github/workflows/deploy.yml builds, tests, and deploys on push to main; deploy-pages.yml publishes this Atlas. Pull requests get no automated build or test run.
MediumNo FCM Push Notifications
FcmToken field exists but no Firebase Cloud Messaging integration.
LowNo Parent Self-Linking
Parent-to-child linking is only via database FK. No QR code flow.
LowNo Avatar/Image Upload
No profile image capability in API or Flutter spec.
LowNo PDF Report Generation
Spec describes QuestPDF but not implemented. Only Excel export.
LowEmpty Improvement Roadmap
docs/Improvement-Roadmap.md has 0 lines.
⚡
Rate Limits
Auth Endpoints
10
requests/min
Teacher Endpoints
30
req/min (5 for upload)
Student/Parent
30
requests/min
Admin Endpoints
20
requests/min
Payment Endpoints
20
requests/min
Cache TTL
2m
dashboards, 1m leaderboard
◆
4-Layer Clean Architecture
Dependencies flow INWARD only. Domain has zero dependencies. Every layer depends only on inner layers.
API Layer Infrastructure Layer Application Layer Domain Layer 18 entities · 13 enums · 5 exceptions Zero dependencies — the core of the system Entities User, Teacher, Student, Parent Group, Session, Assessment... Exceptions NotFoundException (404) BadRequestException (400) ConflictException (409) CQRS Use Cases 88 Command/Query + Handler + FluentValidation Pipeline Behaviors LoggingBehavior ValidationBehavior 13 Repository Interfaces IUserRepository... 11 Service Interfaces ICacheService... Implementations 13 Repositories EF Core + SQL Server AsNoTracking for reads No SaveChanges in repos AppDbContext 17 DbSets TPT inheritance Guid.CreateVersion7() UnitOfWork Explicit SaveChanges Concurrency handling SqlException mapping Auth + Email JwtService + BCrypt SmtpEmailService ConsoleEmailService 4 Hangfire Jobs ProcessExcel · Leaderboard BadgeAwarding · Cleanup File + Cache + Metrics LocalFileStorageService CacheService · ApiMetrics API Components 8 Controllers · 98 endpoints Auth · Teacher · Student Parent · Payment · Admin Subject · Dev 4 Middleware SecurityHeaders Localization (?lang=ar) RequestLogging RequestId GlobalExceptionHandler 25+ exception types → RFC 9457 ProblemDetails Rate Limiters (7) Auth=10, Teacher/Student=30 Payment/Admin=20 CORS · Health · Serilog FlutterApp policy /health + /health/ready OpenAPI + Scalar Auto-generated docs External Services SQL Server (Database) SMTP (Email) Seq (Logging) Hangfire (Background Jobs) Domain implements API Infrastructure Application Domain External
Why This Architecture?
Clean Architecture
Why: Domain logic never depends on infrastructure. You can swap SQL Server for PostgreSQL, or SMTP for SendGrid, without changing a single handler.
Trade-off: More files, more boilerplate. A simple CRUD needs 4+ files instead of 1.
CQRS via MediatR
Why: Separates read and write logic. Each use case is a single, testable class. No "god controllers".
Trade-off: MediatR adds indirection. Finding "what happens when I call this endpoint" requires tracing through pipeline → handler.
Repository Pattern
Why: Testability — mock repositories in unit tests. Swap EF Core for Dapper without changing handlers.
Trade-off: Thin repositories in this project (just EF calls). Some argue this adds unnecessary abstraction layers.
Unit of Work
Why: Handlers call SaveChanges explicitly — no accidental saves. Transaction control is in the handler, not hidden in repos.
Trade-off: Easy to forget SaveChangesAsync. No automatic rollback on handler failure (relies on EF Core change tracking).
📁
Project Layout
5 projects in the solution. Dependencies flow inward only.
TrackToImprove.API
Entry point. Controllers, middleware, Program.cs, configuration.
Controllers/ (8 files) Middleware/ (4 files) Configuration/ (4 files) ExceptionHandler/ (1 file) Program.cs (401 lines) appsettings.json appsettings.Development.json
TrackToImprove.Application
Business logic. CQRS handlers, validators, DTOs, interfaces.
UseCases/ (15 domains, 88 files) Behaviors/ (LoggingBehavior, ValidationBehavior) Common/ (CacheKeys, PaginatedResult) DTOs/ (16 subdirectories) Interfaces/ (Repositories/ + Services/) Services/ (LocalizationService)
TrackToImprove.Domain
Core domain. Entities, enums, exceptions, constants. Zero dependencies.
Entities/ (18 files) Enums/ (13 files) Exceptions/ (5 files) Constants/ (AuthConstants)
TrackToImprove.Infrastructure
Data access & external services. EF Core, repos, auth, email, Hangfire.
Persistence/ (DbContext, UnitOfWork) Repositories/ (13 files) Services/ (8 files) Auth/ (JwtService, PasswordHasher) Email/ (SmtpEmailService, ConsoleEmailService) Jobs/ (4 Hangfire jobs) EntityConfigurations/ (18 files) Migrations/ (16 files) SeedData/ (4 SQL files)
Dependency Rules
API ──depends on──▶ Application ──depends on──▶ Domain
Infrastructure ──depends on──▶ Application
✗ Domain NEVER depends on anything
✗ Application NEVER depends on Infrastructure
✗ API NEVER depends on Infrastructure directly (only via Application interfaces)
Key Files to Know
API/Program.cs
All wiring, middleware pipeline, DI
Application/.../DependencyInjection.cs
MediatR + validator registration
Infrastructure/.../DependencyInjection.cs
DbContext + repos + services registration
Infrastructure/Persistence/AppDbContext.cs
17 DbSets, TPT config
Infrastructure/Persistence/UnitOfWork.cs
SaveChanges with error handling
API/ExceptionHandler/GlobalExceptionHandler.cs
25+ exception → HTTP mappings
⚡
Request Lifecycle — What Happens When You Call an Endpoint
Click each step to see the code and explanation.
1. Controller Extract JWT, build command 2. MediatR Route to handler 3. LoggingBehavior Log request + timing 4. ValidationBehavior Run FluentValidation 5. Handler Business logic 5a. Check auth/ownership throw ForbiddenException 5b. Call repository Get/Add/Update entity 5c. SaveChangesAsync Via UnitOfWork 5d. Invalidate cache _cache.Remove(key) 5e. Return DTO Never return entity Response flows back: Handler → Controller → OkEnvelope → { data, meta } Exception Paths NotFoundException → 404 BadRequestException → 400 ConflictException → 409 ForbiddenException → 403 ValidationException → 400 + errors[] GlobalExceptionHandler catches all → RFC 9457 ProblemDetails
Example: Creating a Group (Full Code Path)
// 1. Controller (TeacherController.cs) [HttpPost("groups")] [Authorize(Roles = "Teacher")] public async Task<IActionResult> CreateGroupAsync( [FromBody] CreateGroupRequestDto dto, CancellationToken ct) { var teacherId = GetUserId(); // from JWT claims var command = new CreateGroupCommand( dto.Name, dto.AcademicYear, dto.GradeLevel, dto.Period, dto.SubjectId, teacherId, dto.Schedules); var result = await _sender.Send(command, ct); return OkEnvelope(result); // { data, meta } } // 2. Command (CreateGroupCommand.cs) public record CreateGroupCommand( string Name, string AcademicYear, GradeLevel GradeLevel, string Period, Guid SubjectId, Guid TeacherId, List<CreateGroupScheduleRequestDto>? Schedules) : IRequest<GroupResponseDto>; // 3. Handler (CreateGroupCommandHandler.cs) public class CreateGroupCommandHandler : IRequestHandler<CreateGroupCommand, GroupResponseDto> { // Inject: IGroupRepository, ISubjectRepository, // IGroupScheduleRepository, IUnitOfWork, ICacheService public async Task<GroupResponseDto> Handle( CreateGroupCommand request, CancellationToken ct) { var subject = await _subjectRepository .GetByIdAsync(request.SubjectId, ct); if (subject is null) throw new NotFoundException("Subject not found"); var group = new Group { Name = request.Name, TeacherId = request.TeacherId, SubjectId = request.SubjectId, // ... other fields }; await _groupRepository.AddAsync(group, ct); // Process schedules if provided... await _unitOfWork.SaveChangesAsync(ct); _cache.Remove(CacheKeys.TeacherGroups(request.TeacherId)); return new GroupResponseDto(group.Id, group.Name, /* ... */); } } // 4. Validator (CreateGroupCommandValidator.cs) public class CreateGroupCommandValidator : AbstractValidator<CreateGroupCommand> { public CreateGroupCommandValidator() { RuleFor(x => x.Name).NotEmpty().MaximumLength(200); RuleFor(x => x.Period) .Must(p => new[] {"First Term","Second Term"}.Contains(p)); } }
The Pipeline (Automatic for Every Request)
Request enters
→
LoggingBehavior
→
ValidationBehavior
→
Handler executes
→
Response returned
📚
Domain Entities — The Data Model
18 entities, 13 enums. TPT inheritance: User → Teacher/Student/Parent. Click any entity for details.
TPT Inheritance — Why User is the Base
User (base) Id · Email · FullName · Role · PasswordHash Teacher Student Parent +NationalId · Bio · Rating +StudentCode · ParentId +ConsentGiven · Children
Why TPT (Table-Per-Type)? Each role gets its own table with extra columns. Teachers have NationalId and Bio; Students have StudentCode and ParentId. All share auth fields (email, password) from the User table.
Trade-off vs TPH: TPT requires JOINs for queries (slower reads) but uses less storage and keeps role-specific columns clean. TPH (single table) is faster for reads but wastes space with NULLs.
Enums Reference
UserRole: None=0, Student=1, Parent=2, Teacher=3, Admin=4
GradeLevel: SecondaryOne=1, SecondaryTwo=2, SecondaryThree=3
SessionType: Exam=0, Quiz=1, Homework=2, Oral=3, Attendance=4
SessionColumnType: Attendance=0, Score=1, Comment=2
SubscriptionPlan: Free=0, Pro=1, Premium=2
PaymentStatus: Pending=0, Confirmed=1, Disputed=2
NotificationType: NewGrade=0, RankChange=1, BadgeEarned=2, PaymentConfirmed=3, SystemAlert=4
LeaderboardScope: Group=0, Teacher=1, Platform=2
LeaderboardPeriod: Daily=0, Weekly=1, Monthly=2, AllTime=3
EnrollmentStatus: Pending=0, Active=1, Removed=2
AssessmentKind: Written=0, Assignment=1, Quiz=2, Oral=3, Other=4
BadgeType: None=0, StudentOfWeek=1, StudentOfMonth=2, MostImproved=3, PerfectAttendance=4, HighestScore=5, Consistency=6, TopicMaster=7
UploadStatus: Queued=0, Processing=1, Completed=2, Failed=3
+
How to Add a New Feature
Step-by-step guide. Every feature follows the same pattern. Follow these steps in order.
🗃
Database — EF Core + SQL Server
How to add/edit entities, run migrations, and understand the schema.
Adding a New Entity
// Step 1: Create entity in Domain/Entities/ public class Notification { public Guid Id { get; set; } // Guid.CreateVersion7() in handler public Guid UserId { get; set; } public string Title { get; set; } = string.Empty; public string TitleArabic { get; set; } = string.Empty; public string Body { get; set; } = string.Empty; public string BodyArabic { get; set; } = string.Empty; public NotificationType Type { get; set; } public bool IsRead { get; set; } public string? Payload { get; set; } public DateTime SentAt { get; set; } public User User { get; set; } = null!; // Navigation } // Step 2: Add DbSet in AppDbContext.cs public DbSet<Notification> Notifications => Set<Notification>(); // Step 3: Create configuration in EntityConfigurations/ public class NotificationConfiguration : IEntityTypeConfiguration<Notification> { public void Configure(EntityTypeBuilder<Notification> builder) { builder.HasKey(n => n.Id); builder.Property(n => n.Title).HasMaxLength(200).IsRequired(); builder.HasOne(n => n.User).WithMany().HasForeignKey(n => n.UserId); } } // Step 4: Create migration dotnet ef migrations add AddNotificationEntity \ --project src/TrackToImprove.Infrastructure \ --startup-project src/TrackToImprove.API // Step 5: Update database dotnet ef database update \ --project src/TrackToImprove.Infrastructure \ --startup-project src/TrackToImprove.API
Key Database Patterns
IDs
All IDs are Guid.CreateVersion7() — time-ordered UUIDs. Generated in handlers, NOT in entities. Entities have { get; set; } only.
No SaveChanges in Repos
Repositories just call _context.AddAsync() or LINQ queries. Handlers call _unitOfWork.SaveChangesAsync() explicitly.
AsNoTracking for Reads
Read-only queries use .AsNoTracking() — faster, no change tracking overhead. Only use tracking for update scenarios.
Migrations
Always create migrations from CLI, not from code. Project: TrackToImprove.Infrastructure. Startup: TrackToImprove.API.
Entity Relationships
User 1──1 Teacher / Student / Parent (TPT inheritance)
Teacher 1──N Group (teacher owns groups)
Group 1──N Session (group has sessions)
Group 1──N GroupSchedule (weekly schedule)
Group M──M Student via StudentsGroups (join table)
Session 1──N StudentSession (attendance per student)
StudentSession 1──N Assessment (scores per session)
Student M──M Badge via StudentsBadges
Student 1──N Leaderboard (rankings)
Teacher 1──N PaymentRecord
✓
Testing - 922 Tests, 0 Failures
xUnit + Moq + FluentAssertions. Unit tests for handlers, integration tests for controllers.
Unit Tests
// Arrange var repo = new Mock<IGroupRepository>(); var unitOfWork = new Mock<IUnitOfWork>(); var cache = new Mock<ICacheService>(); var handler = new CreateGroupCommandHandler( repo.Object, unitOfWork.Object, cache.Object); var command = new CreateGroupCommand( "Math Group", "2026", GradeLevel.SecondaryOne, "First Term", Guid.NewGuid(), Guid.NewGuid(), null); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.Should().NotBeNull(); result.Name.Should().Be("Math Group"); repo.Verify(r => r.AddAsync(It.IsAny<Group>(), CancellationToken.None), Times.Once); unitOfWork.Verify(u => u.SaveChangesAsync( CancellationToken.None), Times.Once);
Integration Tests
// Uses CustomWebApplicationFactory // InMemory DB + Fake services public class TeacherControllerTests : IClassFixture<CustomWebApplicationFactory> { [Fact] public async Task GetGroups_ReturnsOk() { // Arrange var client = _factory .WithWebHostBuilder(builder => { /* ... */ }) .CreateClient(); var token = TestAuthHelper .GenerateToken(userId, "Teacher"); client.DefaultRequestHeaders.Authorization = new("Bearer", token); // Act var response = await client .GetAsync("/api/v1/teachers/groups"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); } }
Test Helpers
TestDataBuilder
Static factory methods for creating valid test entities. Thread-safe counter for unique values.
TestAuthHelper
Generates JWT tokens for test users with known secret. Used in integration tests.
DatabaseSeeder
Seeds deterministic test data with fixed GUIDs. Same data every test run.
CustomWebApplicationFactory
InMemory EF Core + FakeEmailService + FakeJobService + FakeFileStorage. Sets ASPNETCORE_ENVIRONMENT=Testing.
FakeServices
FakePasswordHasher (no BCrypt), FakeEmailService (logs to console), FakeJobService (no Hangfire), FakeFileStorage (no disk).
NonSeekableStream
Wrapper for testing LoggingBehavior with non-seekable streams (prevents Stream.ReadTimeout).
Running Tests
# Run all tests dotnet test # Run specific test class dotnet test --filter "FullyQualifiedName~CreateGroupCommandHandlerTests" # Run with verbosity dotnet test --verbosity normal # Run and generate coverage report dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
⚙
Configuration — What to Change and Where
Every config file and what it controls.
appsettings.json (base config)
{ "Jwt": { "Issuer": "TrackToImprove", "Audience": "TrackToImprove", "Secret": "SET VIA USER-SECRETS OR ENV VARS — NEVER COMMIT" }, "ConnectionStrings": { "DefaultConnection": "Server=...;Database=TrackToImprove;" }, "Serilog": { "MinimumLevel": "Information" }, "FileStorage": { "BasePath": "uploads", "RetentionDays": 30, "MaxFileSizeMb": 10 }, "Smtp": { "Host": "smtp.gmail.com", "Port": 587, "From": "noreply@track2improve.com" } }
appsettings.Development.json (local overrides)
{ "Jwt": { "Secret": "dev-secret-key-change-in-production" }, "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=TrackToImprove;" }, "BypassEmailVerification": true, "DevAdmin": { "Email": "admin@track2improve.com", "Password": "Admin@t2i@123" } }
Environment Variables (Production)
# JWT Secret Jwt__Secret=your-production-secret # Database ConnectionStrings__DefaultConnection=Server=...;Database=...; # SMTP Smtp__Host=smtp.gmail.com Smtp__Password=your-app-password # Seq Logging Serilog__WriteTo__2__Args__serverUrl=http://seq:8081 # CORS Origins Cors__Origins=https://yourdomain.com
⚖
Trade-offs — Why This and Not That
Every architectural decision has a cost. Here's why we chose what we chose.
⚠
Common Pitfalls — What Will Bite You
Mistakes that look right but are wrong. Read these before writing any code.
◆
Flutter Developer API Reference
98 endpoints · Business context · Display guidance for every property
Auth
Teacher Home
Teacher Profile
Teacher Groups
Teacher Students
Teacher Sessions
Teacher Uploads
Notifications
Leaderboard
Student Home
Student Sessions
Student Profile
Parent Home
Payments
Admin
Subjects
Dev & Ops
POST /api/v1/auth/register Create a new user account
What It Does
Creates a new teacher, student, or parent account. Returns JWT tokens immediately so the user can start using the app. The email must be unique across the entire system. Password is hashed server-side — never stored in plain text. After registration, the user receives a verification email with a 6-digit code.
Auth: Public · Rate: 10 req/min
Request Body
FieldDescription
fullNameFull display name shown in profiles and conversation headers. E.g. "Ahmed Hassan"
emailLogin identifier. Must be unique. Used for all auth flows. E.g. "ahmed@gmail.com"
usernameUnique username, 3-50 characters. Shown in profiles. E.g. "ahmed_h"
passwordMinimum 8 characters. Hashed immediately on the server. E.g. "Teacher@123"
phoneNumberContact phone in E.164 format. E.g. "+201234567890"
dateOfBirthMust be a past date. Used for age verification and profile display.
roleOne of: "Teacher", "Student", "Parent". Determines permissions and which dashboard the user sees.
Response — 201 Created
{ "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "dGhpcyBpcyBhIHJlZnJl...", "expiresAt": "2025-10-01T12:00:00Z", "role": "Teacher", "userId": "b0000000-0000-0000-0000-000000000001" }
Property Reference
PropertyWhat It Means & How to Display
accessTokenJWT bearer token. Send this in the Authorization: Bearer {token} header for all authenticated requests. Store securely in Flutter (e.g. flutter_secure_storage). Expires after the time shown in expiresAt.
refreshTokenLong-lived token used to get a new accessToken without re-entering credentials. Store securely. Send to /auth/refresh-token when the access token expires.
expiresAtUTC timestamp when the accessToken stops working. Use this to schedule an automatic refresh before it expires. Display as relative time if needed: "Expires in 2 hours".
roleDetermines which dashboard to show and which API endpoints are accessible. Route the user to the correct home screen: Teacher → TeacherHomeScreen, Student → StudentHomeScreen, Parent → ParentHomeScreen.
userIdUnique ID of the authenticated user. Useful for client-side operations like showing profile, filtering local cache, or WebSocket subscriptions.
POST /api/v1/auth/login Authenticate with email and password
What It Does
Authenticates a user by email and password. Returns the same token structure as register. Use this for the login screen. If the email is not verified, the server still returns tokens but some endpoints may be restricted.
Auth: Public · Rate: 10 req/min
Request Body
FieldDescription
emailRegistered email address
passwordPlain-text password (verified against stored hash)
Response — 200 OK
{ "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "dGhpcyBpcyBhIHJlZnJl...", "expiresAt": "2025-10-01T12:00:00Z", "role": "Teacher", "userId": "b0000000-0000-0000-0000-000000000001" }
Property Reference
Same structure as register response. See register endpoint above for full property descriptions.
POST /api/v1/auth/refresh-token Get new access token without re-login
What It Does
Exchanges a valid refresh token for a new access token + refresh token pair. Call this automatically when the accessToken expires. The old refresh token is invalidated after use (rotation). If the refresh token is also expired, the user must log in again.
Auth: Public · Rate: 10 req/min
Request Body
FieldDescription
accessTokenThe expired or soon-to-expire access token
refreshTokenThe current valid refresh token
Response — 200 OK
Same AuthResponseDto structure. Store the new tokens and update your expiry tracker.
POST /api/v1/auth/logout Invalidate refresh token
What It Does
Invalidates the refresh token so it can no longer be used to get new access tokens. The access token itself remains valid until it expires. Call this when the user taps "Logout" in the app. Clear all stored tokens locally after a successful call.
Auth: Public · Rate: 10 req/min
Request Body
FieldDescription
refreshTokenThe refresh token to invalidate
Response — 200 OK
{ "message": "Logged out successfully" }
POST /api/v1/auth/forgot-password Send password reset code to email
What It Does
Sends a 6-digit verification code to the user's email address. The code is valid for a limited time. Call this from the "Forgot Password" screen. After calling, navigate to the "Enter Code" screen. Rate-limited to prevent email flooding.
Auth: Public · Rate: 10 req/min
Request Body
FieldDescription
emailThe registered email address to send the reset code to
Response — 200 OK
MessageResponseDto with confirmation message. The actual code is sent via email, not returned in the response.
POST /api/v1/auth/reset-password Complete password reset with code
What It Does
Verifies the 6-digit code from the email and sets the new password. After success, the user can log in with the new password. The reset code is single-use and expires after a short window.
Auth: Public · Rate: 10 req/min
Request Body
FieldDescription
emailThe email address the code was sent to
otpThe 6-digit code from the email
newPasswordNew password, minimum 8 characters
Response — 200 OK
MessageResponseDto. After this, navigate the user to the login screen.
POST /api/v1/auth/verify-email Verify email with 6-digit code
What It Does
Verifies the user's email address using the 6-digit code sent to their inbox. After verification, all features become available. Some endpoints may be restricted until email is verified. The code expires after a short time window.
Auth: Public · Rate: 10 req/min
Request Body
FieldDescription
emailThe email address to verify
codeThe 6-digit verification code from the email
POST /api/v1/auth/resend-verification Resend email verification code
What It Does
Sends a new 6-digit verification code to the user's email. Use this when the user didn't receive the original code or it expired. Rate-limited to prevent email flooding.
Auth: Public · Rate: 10 req/min
Request Body
FieldDescription
emailThe email address to resend the code to
POST /api/v1/auth/change-password Change password while logged in
What It Does
Changes the authenticated user's password. Requires the current password for verification. After success, continue using the same tokens — no re-login needed. Use this from a "Change Password" settings screen.
Auth: Bearer token (any role) · Rate: 10 req/min
Request Body
FieldDescription
currentPasswordThe user's current password for verification
newPasswordNew password, minimum 8 characters
GET /api/v1/teachers/home Complete teacher dashboard — one call for the entire home screen
What It Does
Returns everything the teacher needs for their home screen in a single API call. This is the most important endpoint for the teacher app — it powers the main dashboard. The response is cached for 2 minutes on the server. It computes trends by comparing this week's data to last week's data, not month-over-month. The platform comparison section ranks this teacher against all other active teachers on the platform.
Auth: Bearer token (teacher role) · Rate: 30 req/min · Cached: 2 min
Response — 200 OK
{ "profile": { "fullName": "Ahmed Hassan", "email": "ahmed@gmail.com", "subject": "Mathematics", "subscriptionPlan": "Premium", "subscriptionEnd": "2026-03-01T00:00:00Z" }, "overview": { "totalGroups": 5, "activeGroups": 4, "totalStudents": 120, "totalSessionsThisMonth": 12, "pendingPaymentsEGP": 3500.00, "totalUploadsThisMonth": 8, "scoreTrend": 5.2, "attendanceTrend": 0, "trendPercent": 5.2, "topPerformingGroup": "Math 101 - Section A", "topPerformingGroupScore": "87.5%", "studentsAtRiskCount": 3, "uploadStreakWeeks": 6, "classAverageScore": 72.3, "totalSessionsAllTime": 156, "attendanceRate": 0 }, "trends": { "scoreChangePercent": 5.2, "attendanceChangePercent": 2.1, "studentsChange": 8, "scoreTrend": "up", "attendanceTrend": "up" }, "topGroups": { "top": [ { "groupId": "a1b2c3d4-...", "name": "Math 101 - A", "subject": "Mathematics", "gradeLevel": "SecondaryOne", "period": "First Term", "studentCount": 30, "averageScore": 87.5, "isActive": true, "attendanceRate": 92.0, "activeSessions": 12, "lastUploadDate": "2025-09-28T14:30:00Z", "trend": "up" } ], "bottom": [ { "groupId": "c3d4e5f6-...", "name": "Math 101 - C", "subject": "Mathematics", "gradeLevel": "SecondaryThree", "period": "First Term", "studentCount": 25, "averageScore": 52.1, "isActive": true, "attendanceRate": 65.0, "activeSessions": 10, "lastUploadDate": "2025-09-25T10:00:00Z", "trend": "down" } ] }, "recentUploads": [ { "uploadId": "f1e2d3c4-...", "fileName": "week1_scores.xlsx", "groupName": "Math 101 - A", "status": "Completed", "rowsProcessed": 30, "uploadedAt": "2025-09-28T14:30:00Z" } ], "topStudents": [ { "gradeLevel": "SecondaryOne", "groupName": "Math 101 - A", "students": [ { "studentId": "s1t2u3d4-...", "fullName": "Sara Ali", "studentCode": "STU001", "rank": 1, "avgScore": 95.0, "attendanceRate": 100.0 } ] } ], "unreadNotifications": 3, "platformComparison": { "yourAverageScore": 72.3, "schoolAverageScore": 68.1, "yourRank": 12, "totalTeachers": 85, "performanceLabel": "Above Average" } }
Property Reference
PropertyWhat It Means & How to Display
profile — Teacher's identity and subscription
profile.fullNameThe teacher's display name. Show as the main title on the dashboard header. E.g. "Good morning, Ahmed"
profile.emailContact email. Show in profile section or settings.
profile.subjectThe primary subject this teacher teaches. Show as a subtitle or chip. E.g. "Mathematics"
profile.subscriptionPlanCurrent plan: "Free", "Pro", or "Premium". Show as a badge/chip. Controls which features are accessible.
profile.subscriptionEndWhen the subscription expires. Show as "Expires Mar 2026" or countdown. If null, the teacher is on the free plan.
overview — Key metrics at a glance
overview.totalGroupsTotal groups this teacher has ever created (including inactive). Show as a stat card: "5 Groups"
overview.activeGroupsGroups that are currently active (not soft-deleted). The number that matters for daily use. Show alongside total: "4 active"
overview.totalStudentsTotal students across all active groups. Sum of all enrolled students. Show as stat card: "120 Students"
overview.totalSessionsThisMonthHow many sessions the teacher conducted this calendar month. Show as stat card. Compare to last month if possible.
overview.pendingPaymentsEGPTotal money in Egyptian Pounds that parents owe but haven't confirmed yet. Show with warning color if > 0: "3,500 EGP pending"
overview.totalUploadsThisMonthNumber of Excel files uploaded this month. Shows how active the teacher is with data entry.
overview.scoreTrendAbsolute change in average score vs previous month. Positive = improvement. E.g. 5.2 means scores went up 5.2 points. Display as "+5.2 pts" with green color.
overview.topPerformingGroupName of the group with the highest average score. Show as a highlight: "Your best: Math 101 - A"
overview.topPerformingGroupScoreThe average score of that top group. Show alongside the name: "87.5% avg"
overview.studentsAtRiskCountStudents scoring below 50% or with attendance below 60%. This is a critical alert. Show as red badge: "3 students need attention". Tap to open the dedicated needs-attention screen (GET /teachers/students/needs-attention).
overview.uploadStreakWeeksConsecutive weeks the teacher uploaded at least one score sheet. Gamification metric. Show with fire/trophy icon: "6-week streak!"
overview.classAverageScoreAverage score across ALL students in ALL the teacher's groups (0-100). The single most important number. Display as large circular progress indicator. Color: green ≥80, yellow 60-79, red <60.
overview.totalSessionsAllTimeTotal sessions ever conducted. Shows experience/tenure. Display as subtle stat: "156 total sessions"
trends — Week-over-week comparison
trends.scoreChangePercentPercentage change in average score from last week to this week. E.g. 5.2 means scores improved 5.2% this week. Display as "+5.2%" in green with ↑ arrow, or "-3.1%" in red with ↓ arrow.
trends.attendanceChangePercentPercentage change in attendance rate from last week to this week. Same display logic as scoreChangePercent.
trends.studentsChangeNet change in unique students who attended sessions this week vs last week. E.g. 8 means 8 more students showed up this week. Display as "+8 students" badge.
trends.scoreTrendDirection of scores over the last week. Values: "up", "down", "stable". Display as colored arrow: ↑ green, ↓ red, → gray.
trends.attendanceTrendSame as scoreTrend but for attendance. Same arrow treatment.
topGroups — Best and worst performing groups
topGroups.topTop 2 groups by average score (only groups with students). Show as a "Top Performers" list with rank, name, and score.
topGroups.bottomBottom 2 groups by average score. Show as "Needs Improvement" list. Use this to prompt the teacher to focus on struggling groups.
topGroups.*.averageScoreGroup's average score (0-100). Display as progress bar. Color: green ≥80, yellow 60-79, red <60.
topGroups.*.trendScore direction: "up", "down", "stable". Show as arrow icon next to the score.
topGroups.*.attendanceRateWhat % of students attended sessions in this group. Show as percentage. Below 70% should be flagged.
recentUploads — Latest file uploads
recentUploadsLast 3 uploaded Excel files. Show as a list with file name, group name, and status badge. Tap to view upload details.
recentUploads.*.statusProcessing status: "Queued", "Processing", "Completed", or "Failed". Show as colored badge: green=Completed, yellow=Processing, red=Failed.
recentUploads.*.rowsProcessedHow many student rows were successfully processed. Show as "30/30 students" or "28/30 (2 failed)".
topStudents — Best students per grade
topStudentsFor each grade level, the top 3 students from the highest-performing group in that grade. Show as expandable sections by grade. Each student shows rank, name, score, and attendance.
topStudents.*.rankStudent's rank in their group (1-based). Display as rank badge: #1 gold, #2 silver, #3 bronze.
Other fields
unreadNotificationsCount of unread notifications. Show as red badge on the notification bell icon. Tap to open notifications screen.
needsAttention — movedRemoved from /home. Use the dedicated paginated endpoint GET /teachers/students/needs-attention: richer detail (studentCode, groupId, gradeLevel, missedSessionsCount, parent contact), filters (groupId, gradeLevel, reason=low_score|low_attendance|low_score_low_attendance, search), 20/page.
platformComparisonThis teacher's ranking compared to all other active teachers. Null if computation fails. Show as a comparison card: "You rank #12 out of 85 teachers".
platformComparison.performanceLabelHuman-readable label: "Above Average" (≥110% of school avg), "Average" (90-110%), or "Below Average" (<90%). Display as colored badge.
GET /api/v1/teachers/students/needs-attention Paginated list of at-risk students — low score / low attendance, with parent contact
What It Does
Powers the dedicated Students Needing Attention screen (opened by tapping the overview.studentsAtRiskCount badge on /home). Returns a paginated list of students with average score < 50% or attendance rate < 60% across the teacher's active groups. Each entry includes the failure reason, current score, attendance rate, missed session count, and parent contact (name + phone) for one-tap calls.
Auth: Bearer token (Teacher role) · Rate: 30 req/min · Pagination: 20/page (max 100)
Query Parameters
ParameterTypeDefaultDescription & Flutter Usage
groupIdGuid?nullLimit to one group. Pass from a group dropdown on the attention screen.
gradeLevelstring?nullFilter by grade: SecondaryOne, SecondaryTwo, SecondaryThree.
reasonstring?nulllow_score, low_attendance, or low_score_low_attendance (case-insensitive). Map to filter chips — use the row's reasonCode as the chip value, never the display text. Unknown values (incl. localized labels like Low score & Low attendance) return 400.
searchstring?nullFilters by student name, student code, or group name (case-insensitive).
page / pageSizeint1 / 20Pagination. Max pageSize is 100.
Response — 200 OK (PaginatedResult<TeacherNeedsAttentionDetailDto>)
{ "items": [ { "studentId": "a1b2c3d4-...", "fullName": "Omar Khaled", "studentCode": "STU-042", "groupId": "9a8b7c6d-...", "groupName": "Physics - Sec 2 (A)", "gradeLevel": "SecondaryTwo", "reason": "Low score & Low attendance", "reasonCode": "low_score_low_attendance", "currentScore": 42.5, "attendanceRate": 0.4, "missedSessionsCount": 3, "parentName": "Khaled Hassan", "parentPhoneNumber": "+201012345678" } ], "page": 1, "pageSize": 20, "totalCount": 4 }
Property Reference
PropertyWhat It Means & How to Display
items[].fullNameStudent's name. Show as card title with a red alert icon.
items[].studentCodeStudent's unique code. Show as subtitle chip for identification.
items[].groupNameGroup the student belongs to. Show as a secondary chip.
items[].reasonLocalized display-only failure text (EN/AR): "Low score", "Low attendance", or "Low score & Low attendance". Display as alert subtitle in red. Never send back as the reason filter — it returns 400.
items[].reasonCodeMachine token matching the row: low_score, low_attendance, or low_score_low_attendance. Use as the filter-chip value and as the ?reason= query param when re-fetching.
items[].currentScoreCurrent average (0-100). Show as progress bar; always < 50 for low-score entries.
items[].attendanceRateAttendance as a 0-1 fraction rounded to 1 decimal (0.4 = 40%). Low-attendance entries are always < 0.60.
items[].missedSessionsCountSessions the student missed. Show as "3 missed sessions" badge; 0 means attendance is the issue, not absences.
items[].parentName / parentPhoneNumberParent contact for follow-up. Render parentPhoneNumber as a tel: action button. Null if the student has no linked parent.
items are sortedLowest score first, then lowest attendance — worst cases at the top. Don't re-sort client-side.
GET /api/v1/teachers/dashboard Dedicated analytics & charts endpoint — timelines, distributions, benchmarks & filters
What It Does
Powers the dedicated Teacher Analytics & Reports screen. While /teachers/home provides a fast lightweight summary for the initial tab, this endpoint returns deep structured datasets designed specifically for Flutter charting widgets (fl_chart, syncfusion_flutter_charts). It supports flexible multi-criteria filtering by group, grade level, academic period, time range, custom date intervals, and student search. Caching is key-aware per filter selection with a 2-minute TTL. Zero N+1 queries.
Auth: Bearer token (Teacher role) · Rate: 30 req/min · Cached: 2 min (per filter hash)
Query Parameters (Optional Filters)
ParameterTypeDefaultDescription & Flutter Usage
groupIdGuid?nullFilter all chart data and rankings to a single group. Pass from group dropdown selector.
gradeLevelstring?nullFilter by grade level: SecondaryOne, SecondaryTwo, or SecondaryThree.
periodstring?nullFilter by term: First Term or Second Term.
timeRangestring?"30d"Preset interval: 7d (last 7 days), 30d (last 30 days), 90d (last quarter), term (current 4-month term), all (all-time), or custom. Map to time range filter chips.
dateFromDateTime?nullStart date in ISO-8601 (e.g. 2026-08-01T00:00:00Z). Required if timeRange=custom.
dateToDateTime?nullEnd date in ISO-8601. Used with custom date range picker.
searchstring?nullSearch query to filter student names and student codes in the topStudents list. Does not affect overview counts — studentsAtRiskCount is search-independent by design.
studentsPageint1Page number for the paginated topStudents list. Chart sections are never paginated.
studentsPageSizeint10Items per page for topStudents (max 100, clamped server-side). Keep filters identical between pages — the 2-minute cache is shared per filter combination, so paging is as cheap as the first call.
Response — 200 OK
{ "filterApplied": { "groupId": null, "groupName": null, "gradeLevel": null, "period": "First Term", "timeRange": "30d", "dateFrom": "2026-08-20T00:00:00Z", "dateTo": "2026-09-19T23:59:59Z", "search": null }, "overview": { "totalGroups": 6, "activeGroups": 5, "totalStudents": 145, "totalSessionsInPeriod": 24, "averageClassScore": 78.4, "overallAttendanceRate": 89.6, "pendingPaymentsEGP": 3850.00, "studentsAtRiskCount": 4, "uploadStreakWeeks": 7 }, "trends": { "scoreChangePercent": 4.8, "attendanceChangePercent": 2.1, "studentsChange": 12, "scoreTrend": "up", "attendanceTrend": "up" }, "scoreTimelineChart": [ { "date": "2026-08-25T10:00:00Z", "label": "Aug 25", "sessionTitle": "Genetics Quiz #1", "groupName": "Biology - Sec 2", "averageScore": 75.5, "highestScore": 98.0, "lowestScore": 45.0, "attendanceRate": 92.0 }, { "date": "2026-09-05T10:00:00Z", "label": "Sep 05", "sessionTitle": "Midterm Exam", "groupName": "Biology - Sec 2", "averageScore": 81.2, "highestScore": 100.0, "lowestScore": 52.0, "attendanceRate": 96.0 } ], "attendanceTimelineChart": [ { "date": "2026-08-25T10:00:00Z", "label": "Aug 25", "sessionTitle": "Genetics Quiz #1", "groupName": "Biology - Sec 2", "attendedCount": 46, "absentCount": 4, "totalStudents": 50, "attendanceRate": 92.0 } ], "scoreDistribution": { "excellent": { "count": 35, "percentage": 24.1, "range": "85% - 100%" }, "veryGood": { "count": 52, "percentage": 35.9, "range": "75% - 84.9%" }, "good": { "count": 38, "percentage": 26.2, "range": "65% - 74.9%" }, "pass": { "count": 16, "percentage": 11.0, "range": "50% - 64.9%" }, "struggling": { "count": 4, "percentage": 2.8, "range": "< 50%" }, "totalEvaluated": 145 }, "studentHealthStatus": { "excelling": { "count": 35, "percentage": 24.1, "label": "Excelling" }, "onTrack": { "count": 82, "percentage": 56.6, "label": "On Track" }, "needsPush": { "count": 24, "percentage": 16.5, "label": "Needs Push" }, "atRisk": { "count": 4, "percentage": 2.8, "label": "At Risk" } }, "groupPerformanceComparison": [ { "groupId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "groupName": "Biology - Secondary 2 (Group A)", "gradeLevel": "SecondaryTwo", "studentCount": 48, "averageScore": 84.2, "attendanceRate": 94.5, "sessionsCount": 8, "trend": "up" } ], "assessmentCategoryBreakdown": [ { "category": "Exams", "averageScore": 79.5, "totalAssessments": 145 }, { "category": "Quizzes", "averageScore": 74.2, "totalAssessments": 435 }, { "category": "Homework", "averageScore": 88.0, "totalAssessments": 290 } ], "gradeLevelDistribution": [ { "gradeLevel": "SecondaryTwo", "groupsCount": 3, "studentsCount": 90, "averageScore": 81.0 } ], "platformComparison": { "yourAverageScore": 78.4, "schoolAverageScore": 72.0, "yourRank": 3, "totalTeachers": 18, "performanceLabel": "Above Average" }, "topStudents": { "items": [ { "studentId": "018f4a12-...", "fullName": "Ali Hassan", "studentCode": "STU-003", "rank": 1, "avgScore": 96.5, "attendanceRate": 100.0 } ], "page": 1, "pageSize": 10, "totalCount": 145 }, "recentUploads": [ { "uploadId": "018f4a25-...", "fileName": "midterm_scores.xlsx", "groupName": "Biology - Sec 2 (A)", "status": "Completed", "rowsProcessed": 48, "uploadedAt": "2026-09-18T14:30:00Z" } ] }
Property Reference & Flutter Widget Mapping
PropertyBusiness Meaning & Flutter Display Guidance
filterApplied — Active Filter Echo & State Sync
filterApplied.groupIdSelected group filter ID (null if all groups). Use to highlight the active group in your DropdownButton.
filterApplied.groupNameDisplay name of the filtered group (null if all groups). Use as the header sub-title.
filterApplied.gradeLevelSelected grade level filter string (SecondaryOne, SecondaryTwo, SecondaryThree, or null).
filterApplied.periodSelected academic term filter string (e.g. "First Term").
filterApplied.timeRangeNormalized time range: "7d", "30d", "90d", "term", "all", "custom". Use to activate the matching FilterChip.
filterApplied.dateFrom / dateToEffective UTC start and end bounds computed by the server for all time-series charts.
filterApplied.searchActive search string applied to the topStudents list. Echo into the SearchBar. Overview counts remain unfiltered.
overview & trends — KPI Cards & Delta Badges
overview.averageClassScoreWeighted average score percentage (0-100) across all filtered sessions. Display inside a CircularPercentIndicator or large KPI card.
overview.overallAttendanceRateOverall attendance rate percentage across all filtered groups. Display with an attendance icon and progress bar.
overview.totalSessionsInPeriodTotal number of class sessions conducted in the active date window.
overview.totalStudents / activeGroupsActive students and groups enrolled under this teacher.
overview.pendingPaymentsEGPTotal unconfirmed payments in EGP. Show in amber badge if > 0 with a quick navigation button to Payments.
overview.studentsAtRiskCountCount of students with score < 50% or attendance < 60%. Search-independent by design. Display with a red pulsing badge; tap to open the paginated GET /teachers/students/needs-attention screen.
overview.uploadStreakWeeksConsecutive weekly uploads count. Render with a flame/streak badge (e.g. "7-week streak 🔥").
trends.scoreChangePercentWeek-over-week score delta percentage. Render as "+4.8%" in green (up) or "-2.1%" in red (down).
trends.attendanceChangePercentWeek-over-week attendance delta percentage with arrow indicators.
scoreTimelineChart — Score Progression (Line / Area Chart)
scoreTimelineChartOrdered list of session points for fl_chart.LineChart. Map index/date to X-axis and score to Y-axis.
scoreTimelineChart.*.dateSession date. Use for X-axis timestamps or tooltip dates.
scoreTimelineChart.*.labelPre-formatted date label (e.g. "Aug 25"). Perfect for AxisTitles.bottomTitles.
scoreTimelineChart.*.sessionTitleSession topic or exam title for interactive touch tooltips (LineTouchTooltipData).
scoreTimelineChart.*.groupNameGroup name where the session took place. Useful for multi-group legend.
scoreTimelineChart.*.averageScorePrimary Y-axis plot line (FlSpot(x, averageScore)). Value ranges 0-100.
scoreTimelineChart.*.highestScoreUpper bound score in this session. Render as secondary light dotted line or shaded range area.
scoreTimelineChart.*.lowestScoreLowest score in this session. Highlights score variance.
attendanceTimelineChart — Attendance Breakdown (Stacked Bar Chart)
attendanceTimelineChartOrdered list of session attendance for fl_chart.BarChart.
attendanceTimelineChart.*.attendedCountNumber of students present. Render as bottom bar segment in green.
attendanceTimelineChart.*.absentCountNumber of students absent. Render as stacked top bar segment in red/gray.
attendanceTimelineChart.*.attendanceRatePercentage attendance (0-100%). Display in bar tooltip.
scoreDistribution — Grade Brackets (Histogram / Donut Chart)
scoreDistribution5 standard score brackets: excellent (85-100%), veryGood (75-84.9%), good (65-74.9%), pass (50-64.9%), and struggling (<50%).
scoreDistribution.*.countNumber of assessment grades in this bracket. Map to histogram bar heights.
scoreDistribution.*.percentagePercentage of total evaluated scores in this bracket. Map to PieChartSectionData for donut charts.
scoreDistribution.*.rangeLegend range label string (e.g. "85% - 100%").
studentHealthStatus — Student Intervention Gauges (Donut / Status Pie)
studentHealthStatusCategorizes enrolled students into 4 health tiers: excelling (score ≥ 85 & att ≥ 90), onTrack (score ≥ 65 & att ≥ 75), needsPush (50-65%), and atRisk (< 50% score or < 60% att).
studentHealthStatus.*.count / percentageCount and percentage for status donut chart or horizontal segment progress bar.
groupPerformanceComparison — Group Benchmark (Multi-Bar / Radar)
groupPerformanceComparisonList of teacher groups with aggregated metrics. Map to horizontal bar chart or comparison table.
groupPerformanceComparison.*.averageScoreGroup's average score across all sessions in period.
groupPerformanceComparison.*.attendanceRateGroup's overall attendance rate %.
groupPerformanceComparison.*.trendLocalized trend label: "up", "stable", or "down". Display with directional arrow.
assessmentCategoryBreakdown — Exam vs Quiz vs Homework (Polar / Bar)
assessmentCategoryBreakdownPerformance averages grouped by session type (Exams, Quizzes, Homework, Other). Render as a Radar/Spider chart or category bar breakdown.
gradeLevelDistribution — Secondary 1 / 2 / 3 Breakdown
gradeLevelDistributionDistribution of students, groups, and average score across SecondaryOne, SecondaryTwo, and SecondaryThree.
platformComparison — School Benchmark Radar
platformComparisonCompares this teacher against school-wide averages. Shows yourAverageScore vs schoolAverageScore, yourRank, totalTeachers, and performanceLabel ("Above Average", "Average", "Below Average").
topStudents — Actionable List (paginated)
topStudents.itemsTop ranked students filtered/searched. Shows rank (#1 gold, #2 silver, #3 bronze), full name, student code, score, and attendance.
topStudents.totalCount / page / pageSizeDrive infinite-scroll or page buttons via studentsPage / studentsPageSize query params (pageSize max 100). Chart sections are never paginated.
overview.studentsAtRiskCountAt-risk student count (score < 50% or attendance < 60%), independent of search. Tap to open the paginated GET /teachers/students/needs-attention screen for the full detail list.
recentUploadsLast 5 Excel score uploads with status, file name, rows processed, and timestamp. For full history use the paginated GET /teachers/upload-logs.
GET /api/v1/teachers/profile Get the authenticated teacher's full profile
What It Does
Returns the complete profile of the logged-in teacher including subscription details, subject, rating, and personal info. Use this for the Profile screen and Settings screen. The rating is on a 0-5 scale and is computed from student feedback (if implemented).
Auth: Bearer token (teacher role) · Rate: 30 req/min
Response — 200 OK
{ "id": "b0000000-0000-0000-0000-000000000001", "fullName": "Ahmed Hassan", "email": "ahmed@gmail.com", "username": "ahmed_h", "phoneNumber": "+201234567890", "dateOfBirth": "1985-06-15T00:00:00Z", "nationalId": "28506151234567", "subscriptionPlan": "Premium", "subscriptionEnd": "2026-03-01T00:00:00Z", "bio": "Math teacher with 10 years experience", "rating": 5, "subjectName": "Mathematics", "createdAt": "2024-01-15T00:00:00Z" }
Property Reference
PropertyWhat It Means & How to Display
idTeacher's unique identifier. Use for API calls that require a teacher ID. Not typically displayed.
fullNameDisplay name shown across the app — in headers, conversation, student views. Show as the main profile name.
emailLogin email. Show in profile settings. Used for notifications and password reset.
usernameUnique handle. Could be shown as @ahmed_h in profiles.
phoneNumberContact phone in E.164 format. Show in contact section. Parents may use this to reach the teacher.
dateOfBirthDate of birth. Format as "Jun 15, 1985" or compute age: "40 years old".
nationalIdNational ID number. Mask in UI: "2850****5678". Only visible to the teacher themselves.
subscriptionPlanCurrent plan: "Free", "Pro", or "Premium". Show as colored chip. Controls feature access — e.g. Free plan may limit group count.
subscriptionEndWhen subscription expires. Show as "Expires Mar 1, 2026" or "Renews in 5 months". Null for free plan.
bioShort biography. Show as multi-line text in profile. Parents and students can see this.
ratingTeacher rating on 0-5 scale. Display as 5 stars (filled/empty). Use for sorting in teacher discovery.
subjectNamePrimary subject taught. Show as chip/badge. E.g. "Mathematics"
createdAtWhen the account was created. Format as "Member since Jan 2024". Shows tenure/experience.
PUT /api/v1/teachers/profile Update the authenticated teacher's profile
What It Does
Updates the teacher's profile fields. Only the fields you send are updated (partial update). If you change the email, the new email must be re-verified. Use this from the "Edit Profile" screen.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Request Body
FieldDescription
fullNameUpdated display name (required)
phoneNumberUpdated phone number (required)
emailNew email address (optional). If changed, triggers re-verification flow.
bioUpdated biography (optional, teacher-only field)
GET /api/v1/teachers/groups List all groups owned by the authenticated teacher
What It Does
Returns all study groups the teacher has created. Each group includes student count, average score, attendance rate, and trend. Use this for the "My Groups" screen. Groups are sorted by creation date (newest first). Inactive (soft-deleted) groups are included but should be visually distinguished.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Response — 200 OK
List of GroupResponseDto objects. See the first item in the JSON example below for the full structure.
[ { "id": "a1b2c3d4-...", "name": "Math 101 - Section A", "academicYear": "2025-2026", "gradeLevel": "SecondaryOne", "period": "First Term", "isActive": true, "teacherId": "b0000000-...", "subjectName": "Mathematics", "createdAt": "2025-09-01T00:00:00Z", "studentCount": 30, "averageScore": 87.5, "attendanceRate": 92.0, "activeSessions": 12, "lastUploadDate": "2025-09-28T14:30:00Z", "trend": "up", "schedules": [ { "id": "sch1-...", "dayOfWeek": "Monday", "startTime": "08:00", "endTime": "10:00", "isActive": true } ] } ]
Property Reference
PropertyWhat It Means & How to Display
idGroup's unique identifier. Use for all group-related API calls (sessions, students, uploads).
nameDisplay name shown to students and parents. E.g. "Math 101 - Section A". Show as the group card title.
academicYearAcademic year string. E.g. "2025-2026". Show as chip/badge on group cards.
gradeLevelEgyptian secondary school year. Values: "SecondaryOne" (1st year), "SecondaryTwo" (2nd year), "SecondaryThree" (3rd year). Show as chip. Used for filtering and grouping.
periodTeaching period. Values: "First Term" or "Second Term". Show as chip. Used for organizing sessions within an academic year.
isActiveWhether the group is active. Inactive groups are soft-deleted and hidden from student-facing lists. Show inactive groups grayed out with an "Archived" badge.
subjectNameThe subject this group teaches. E.g. "Mathematics". Show as chip. Used for filtering.
studentCountNumber of currently enrolled students. Show as stat: "30 students". Tap to see the student list.
averageScoreAverage score across all students in this group (0-100). Display as progress bar. Color: green ≥80, yellow 60-79, red <60.
attendanceRateWhat % of students attended sessions in this group. Display as percentage. Below 70% should be flagged in red.
activeSessionsNumber of sessions conducted in this group. Shows activity level.
lastUploadDateWhen the last score sheet was uploaded. Show as relative time: "2 days ago". Null if never uploaded.
trendScore direction: "up" (improving), "down" (declining), "stable". Show as arrow icon. Determined by comparing current avg score to threshold: ≥70 = up, ≥50 = stable, <50 = down.
schedulesWeekly schedule entries for this group. Shows which days/times the group meets. Null if no schedules configured. Show as day chips: "Mon 8:00-10:00".
POST /api/v1/teachers/groups Create a new study group
What It Does
Creates a new study group under the authenticated teacher. The teacher is automatically set as the owner. Optionally attach weekly schedule entries on creation. After creating, students can be enrolled into this group.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Request Body
FieldDescription
nameGroup display name, max 200 characters. E.g. "Math 101 - Section A"
academicYearAcademic year string. E.g. "2025-2026"
gradeLevelEnum value: "SecondaryOne", "SecondaryTwo", or "SecondaryThree"
periodMust be "First Term" or "Second Term" (enforced by database CHECK constraint)
subjectIdUUID of an existing subject. Must match a subject from GET /subjects
schedulesOptional list of schedule entries. Each has: dayOfWeek (e.g. "Monday"), startTime (e.g. "08:00"), endTime (e.g. "10:00")
GET /api/v1/teachers/groups/{groupId} Get a single group's full details
What It Does
Returns complete details for a single group including schedules. Use this for the Group Detail screen. The groupId must belong to the authenticated teacher.
Auth: Bearer token (teacher role, owner) · Rate: 30 req/min
PUT /api/v1/teachers/groups/{groupId} Update a group's details
What It Does
Updates a group's metadata. Only the fields you send are updated. Use this from the "Edit Group" screen. Cannot change the teacher owner.
Auth: Bearer token (teacher role, owner) · Rate: 30 req/min
Request Body
FieldDescription
nameNew group name (optional)
academicYearNew academic year (optional)
gradeLevelNew grade level (optional)
periodNew period (optional)
subjectIdNew subject (optional)
isActiveSet to false to soft-delete/archive the group (optional)
DELETE /api/v1/teachers/groups/{groupId} Soft-delete a group (deactivate)
What It Does
Soft-deletes a group by setting isActive = false. The group is not actually removed from the database — it's hidden from student-facing lists. All enrolled students lose access. The teacher can still see it in their group list with an "Archived" badge. Can be reactivated by updating isActive to true.
Auth: Bearer token (teacher role, owner) · Rate: 30 req/min
POST /api/v1/teachers/groups/{groupId}/duplicate Clone a group with all its schedules
What It Does
Creates a copy of an existing group with the same subject, grade level, period, and schedules. The new group gets a different name and starts with zero students and zero sessions. Useful when the teacher wants to create a similar group for a different section.
Auth: Bearer token (teacher role, owner) · Rate: 30 req/min
Request Body
FieldDescription
newNameName for the cloned group. E.g. "Math 101 - Section B"
POST /api/v1/teachers/groups/{groupId}/schedules Add a weekly schedule entry to a group
What It Does
Adds a new weekly schedule entry to a group (e.g. "Every Monday 8:00-10:00"). Groups can have multiple schedule entries for different days. Use this from the group edit screen when the teacher adds class times.
Auth: Bearer token (teacher role, owner) · Rate: 30 req/min
Request Body
FieldDescription
dayOfWeekDay name: "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
startTimeStart time in HH:mm format. E.g. "08:00"
endTimeEnd time in HH:mm format. Must be after startTime. E.g. "10:00"
PUT /api/v1/teachers/schedules/{scheduleId} Update a schedule entry
What It Does
Updates the day, start time, or end time of an existing schedule entry. Use when the teacher reschedules a class time.
Auth: Bearer token (teacher role, owner) · Rate: 30 req/min
DELETE /api/v1/teachers/schedules/{scheduleId} Remove a schedule entry
What It Does
Permanently deletes a schedule entry. Use when the teacher removes a class time from the group's weekly schedule.
Auth: Bearer token (teacher role, owner) · Rate: 30 req/min
GET /api/v1/teachers/groups/{groupId}/students List students in a group with progress data
What It Does
Returns the group roster with per-student progress: attendance, written/assignment averages, overall progress, and recent assessments. Pending and active memberships are included (removed ones are excluded), so the screen can prompt approvals. Attendance counts only recorded attendance sessions inside each membership window.
Auth: Bearer token (teacher role, owner) · Rate: 30 req/min
Response — 200 OK
[ { "studentId": "s1t2u3d4-...", "studentName": "Sara Ali", "studentCode": "STU001", "groupId": "g1r2o3u4-...", "groupName": "Biology - Secondary 2", "enrollmentStatus": "Active", "totalSessions": 12, "attendedSessions": 11, "attendancePercentage": 91.7, "averageWrittenScore": 86.0, "averageAssignmentScore": 89.0, "overallProgressPercentage": 87.5, "recentAssessments": [ { "assessmentId": "...", "examName": "Unit 3 Written", "topic": "Mitosis", "kind": "Written", "rawScore": 18, "maxScore": 20, "scorePercentage": 90, "teacherComment": "Excellent", "sessionDate": "2026-09-12T14:00:00" } ] } ]
Property Reference
PropertyWhat It Means & How to Display
studentIdStudent's unique ID. Use for enrollment, assessment, and notification calls.
studentNameList item title.
studentCodeMonospace subtitle for identification.
enrollmentStatusPending amber chip + "Approve" action; Active green; removed rows never appear here.
attendancePercentage0–100. Show a ring/percentage; flag <70% red.
averageWrittenScore / averageAssignmentScore0–100. Two compact stat tiles.
overallProgressPercentage0–100. Primary progress bar.
recentAssessments[].rawScore / maxScoreRender as 18/20; scorePercentage as 90%.
GET /api/v1/teachers/students Search and list all students visible to the teacher
What It Does
Returns a paginated list of students the teacher has a membership with. The default my-students filter includes pending and active memberships; available lists students without a membership in this teacher's groups. Pending students report isEnrolled: false and zeroed metrics. Use this for the "Find Students" or "My Students" screen.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Query Parameters
ParamDescription
searchCase-insensitive contains on the student's full name or email (NOT the student code)
filtermy-students (default) = pending + active in my groups, available = no membership in my groups. Any other value (or empty) falls back to my-students
pagePage number (default: 1)
pageSizeItems per page (default: 20, max: 100)
Property Reference
FieldDisplay guidance
isEnrolledtrue only with an Active membership. Show a "Pending approval" chip when false but enrolledGroups[].status == "Pending"
enrolledGroups[].statusPending amber, Active green, Removed grey
stats.pendingApprovalCountBadge count on the "Approve" action
stats.averageScore / attendanceRateBoth 0–100; hidden or "—" when stats.totalSessions == 0
stats.lastActivityDateRelative time ("3 days ago"); null for pending students
GET /api/v1/teachers/students/by-code/{studentCode} Look up a student by their unique code
What It Does
Finds a student by their unique code, scoped to the calling teacher. Use this before enrolling: alreadyEnrolledWithTeacher tells you whether the code is already in one of your groups. The response is the minimal StudentLookupResponseDto — email and contact data are deliberately excluded. Unknown code returns 404.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Property Reference
FieldDisplay guidance
studentIdPass to POST /teachers/groups/{groupId}/students
fullNameConfirmation card title
studentCodeMonospace subtitle
alreadyEnrolledWithTeachertrue → disable enroll button and show "Already in your groups"
GET /api/v1/teachers/students/{studentId}/progress Get detailed progress for a specific student
What It Does
Returns progress for a single student scoped to groups owned by the calling teacher, optionally narrowed to one group. Attendance and score figures are SQL aggregates: an attendance session counts only if it was recorded during the membership window, and a missing student row counts as absent. No shared group → 403. Use this for the "Student Detail" screen.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Query Parameters
ParamDescription
groupIdFilter progress to one of the teacher's groups (optional). Must be a group owned by the caller.
Property Reference
FieldDisplay guidance
perGroupProgress[].enrollmentStatusSame color mapping as elsewhere (Pending/Active/Removed)
perGroupProgress[].attendancePercentage0–100; show a progress ring
perGroupProgress[].recentAssessments[].rawScore / maxScoreRender as 18/20
perGroupProgress[].recentAssessments[].kindWritten / Assignment / Quiz / Oral / Other badge
perGroupProgress[].recentAssessments[].scorePercentageRender as 90%; color by band
GET /api/v1/teachers/students/{studentId}/parent Get a student's parent contact information
What It Does
Returns the parent's contact info for a student in one of the teacher's groups. Requires a shared group between the calling teacher and the student; otherwise 403 — contact data is never exposed for students outside the teacher's groups. 404 when the student or their parent record does not exist.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Response — 200 OK
FieldDescription
parentIdParent user ID
fullNameParent's full name
emailParent's email address
phoneNumberParent's phone number
POST /api/v1/teachers/groups/{groupId}/students Enroll a student into a group
What It Does
Adds a student to a group as a Pending membership. Pending students are excluded from rosters, sessions, templates, and Excel uploads until the teacher approves the membership. The response carries the permanent studentCode (generated once, never overwritten). Approve via PUT /teachers/groups/{groupId}/students/{studentId}/approve.
Auth: Bearer token (teacher role, group owner) · Rate: 30 req/min
Request Body
FieldDescription
studentIdUUID of the student (required)
Property Reference
FieldDisplay guidance
studentCodeShow in a confirmation sheet so the student can share it
isEnrolledfalse until approval — label the result "Pending approval"
enrolledGroups[].statusPending at this point
POST /api/v1/teachers/enrollstudentintogroup/{studentCode} Enroll a student by code (URL-based)
What It Does
Alternative enrollment endpoint where the student code is in the URL. The groupId is passed as a query parameter. Same behavior as the POST endpoint above. Use this when enrolling from a QR code scan or barcode reader.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Query Parameters
ParamDescription
groupIdUUID of the group to enroll into (required)
DELETE /api/v1/teachers/groups/{groupId}/students/{studentId} Remove a student from a group
What It Does
Soft-unenrolls the student: the membership becomes Removed with removedAt, while every past session and grade stays readable by the student and parent. Only live metrics and rosters drop the student. Re-enrolling later reactivates the same row as Pending and keeps the existing code.
Auth: Bearer token (teacher role, group owner) · Rate: 30 req/min
PUT /api/v1/teachers/groups/{groupId}/students/{studentId}/approve Approve a pending group enrollment
What It Does
Activates the student's Pending membership in a specific group (Status → Active, stamps approvedAt). This is a group-scoped workflow and does not toggle User.IsActive. Approving an already-active membership is idempotent.
Auth: Bearer token (teacher role, group owner) · Rate: 30 req/min
Property Reference
FieldDisplay guidance
groupIdFrom the selected group row — approval is per group, not per student
studentIdFrom the pending student row
messageShow a snackbar, then refresh the roster
GET /api/v1/teachers/subjects/by-name/{name} Look up a subject by name
What It Does
Finds a subject by its English name. Useful when creating a group and the teacher needs the subject ID. Returns the full subject object including ID, Arabic name, and active status.
Auth: Bearer token (teacher role) · Rate: 30 req/min
GET /api/v1/teachers/sessions List per-student session rows across all groups (paginated)
What It Does
Returns a paginated list of student-session rows (one row per student per session), not one row per session. The shape is PaginatedResult<StudentSessionResponseDto> — the same item shape the student sees in their own history. Supports only page and pageSize; there is no groupId, topic, date, or type filter. Group by sessionId client-side to rebuild a session list.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Query Parameters
ParamDescription
page / pageSizePaging (default 1 / 20, max 100)
Item Reference
FieldDescription
items[].sessionIdSession identifier — group rows by this to build a session list
items[].studentId / studentName / studentCodeStudent identity for the row; null when the row is not student-scoped
items[].groupName / sessionDate / sessionTypeSession metadata
items[].attendedAttendance for this student in this session
items[].score / maxScore / scorePercentageRaw score, max, and percentage of this student's best assessment in the session (null when no assessment)
items[].assessments[]Individual assessments with assessmentId, kind, rawScore, maxScore, scorePercentage
POST /api/v1/teachers/groups/{groupId}/sessions Create a new session for a group
What It Does
Creates a new academic session (e.g. a quiz, exam, or homework) under a specific group. Sessions are the core data unit — they hold student assessments, attendance, and scores. You can optionally define custom Excel column headers for this session, or copy headers from a previous session. After creating, mark attendance and add assessments.
Auth: Bearer token (teacher role, group owner) · Rate: 30 req/min
Request Body
FieldDescription
sessionDateWhen the session took place. UTC date. E.g. "2025-09-28"
sessionTypeType of session. Values: "Exam" (formal test), "Quiz" (short pop quiz), "Homework" (assignment), "Oral" (verbal exam), "Attendance" (no grade, just tracking). Determines which icons/labels to show in the UI.
topicWhat was covered. E.g. "Algebra Basics - Chapter 1". Show as the session title.
descriptionOptional additional details about the session content.
headersOptional list of custom assessment columns for the Excel upload. Each header has: key (internal ID), label (display name), labelArabic (Arabic name), columnType (Score/Attendance/Comment), maxScore, sortOrder, isRequired.
copyHeadersFromSessionIdUUID of another session to copy headers from. Useful for repeating the same assessment structure across sessions.
GET /api/v1/teachers/sessions/{sessionId} Get full session details with all student assessments
What It Does
Returns complete details for a single session including every student's attendance status and all their assessment scores. This is the main endpoint for the "Session Detail" screen where the teacher reviews and edits scores.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min
Response — 200 OK
{ "sessionId": "5e5c1a2b-...", "sessionDate": "2025-09-28T00:00:00Z", "sessionType": "Exam", "topic": "Algebra Basics", "description": "Chapter 1 review", "groupName": "Math 101 - A", "subjectName": "Mathematics", "teacherName": "Ahmed Hassan", "studentSessions": [ { "studentSessionId": "9a8b7c6d-...", "studentId": "s1t2u3d4-...", "studentName": "Sara Ali", "studentCode": "STU001", "attended": true, "assessments": [ { "assessmentId": "3f2e1d0c-...", "examName": "Midterm", "topic": "Algebra", "kind": "Written", "rawScore": 95, "maxScore": 100, "scorePercentage": 95, "teacherComment": "Excellent work" } ] } ] }
Property Reference
PropertyWhat It Means & How to Display
sessionIdSession identifier — required for update, delete, report, and export calls.
sessionTypeDetermines the icon and label. Exam = clipboard icon, Quiz = lightning bolt, Homework = pencil, Oral = microphone, Attendance = calendar check.
studentSessions[].studentSessionIdIdentifier of the student-session join row. Use it when updating attendance rows.
studentSessions[].attendedWhether the student was present. Show as checkmark (green) or X (red) in the attendance column.
studentSessions[].assessments[].assessmentIdIdentifier for PUT /teachers/sessions/assessments/{assessmentId}.
studentSessions[].assessments[].kindWritten | Assignment | Quiz | Oral | Other.
studentSessions[].assessments[].rawScoreScore in the scale of maxScore. Render "95 / 100", not a percentage.
studentSessions[].assessments[].scorePercentageScore as a percentage (0-100). Display as colored number: green ≥80, yellow 60-79, red <60.
studentSessions[].assessments[].teacherCommentTeacher's feedback on the student's performance. Display as tooltip or subtitle under the score.
PUT /api/v1/teachers/sessions/{sessionId} Update a session's details
What It Does
Updates a session's metadata (date, topic, description, type). Only the fields you send are updated. Use this from the "Edit Session" screen. Cannot change the group a session belongs to.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min
Request Body
FieldDescription
sessionDateNew date (optional)
topicNew topic (optional)
descriptionNew description (optional)
sessionTypeNew type (optional)
DELETE /api/v1/teachers/sessions/{sessionId} Delete a session
What It Does
Permanently deletes a session and all its associated assessments and attendance records. This is irreversible. Use with confirmation dialog in the UI. The session will disappear from student and parent views.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min
POST /api/v1/teachers/sessions/{sessionId}/attendance Mark attendance for all students in a session
What It Does
Sets the attendance status for every student in the session. Send a list of student IDs with their attended boolean. This replaces any existing attendance data for the session. Use this from the "Mark Attendance" screen after conducting a session.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min
Request Body
{ "attendances": [ { "studentId": "s1t2u3d4-...", "attended": true }, { "studentId": "w1x2y3z4-...", "attended": false } ] }
POST /api/v1/teachers/sessions/{sessionId}/assessments Add an assessment score for a student
What It Does
Adds a new assessment record for a specific student in a session. Each assessment has an exam name, score, max score, and optional comment. A student can have multiple assessments per session (e.g. quiz + homework). Use this from the "Add Score" form.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min
Request Body
FieldDescription
studentIdUUID of the student being assessed
examNameName of the assessment. E.g. "Midterm", "Quiz 1", "Homework Chapter 1"
scorePercentageOptional percentage (0-100). Ignored when rawScore is supplied.
rawScoreOptional raw score in the scale of maxScore. When supplied, the percentage is derived server-side and wins over scorePercentage.
maxScoreMaximum possible score (1-1000). E.g. 100.
kindWritten (default) | Assignment | Quiz | Oral | Other.
teacherCommentFeedback text. E.g. "Excellent work!" or "Needs to practice more". Show to the student/parent.
PUT /api/v1/teachers/sessions/assessments/{assessmentId} Update an existing assessment score or comment
What It Does
Updates the score or comment on an existing assessment. Use this when the teacher needs to correct a score or add/update feedback. Only the fields you send are updated.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Request Body
FieldDescription
scorePercentageNew percentage (optional). Ignored when rawScore is supplied.
rawScoreNew raw score (optional). Converted to a percentage with the assessment's existing maxScore; wins over scorePercentage.
teacherCommentNew comment (optional). E.g. "Updated: Good improvement"
GET /api/v1/teachers/sessions/{sessionId}/report Get a professional session report
What It Does
Returns a formatted report of the session with aggregated stats: average score, attendance rate, highest/lowest scores, score distribution. Use this for the "Session Report" screen or for sharing results with parents.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min
GET /api/v1/teachers/sessions/{sessionId}/avg-score Get the average score for a session
What It Does
Returns aggregate statistics for a session: roster size, attendance, and score distribution. This is not a bare number — the response is a SessionAvgScoreResponseDto object.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min
Response — 200 OK
{ "sessionId": "5e5c1a2b-...", "sessionType": "Exam", "sessionDate": "2025-09-28T00:00:00Z", "groupName": "Math 101 - A", "totalStudents": 30, "attendedStudents": 27, "attendanceRate": 90.0, "averageScore": 78.5, "minScore": 42, "maxScore": 98, "totalAssessments": 27 }
GET /api/v1/teachers/sessions/{sessionId}/headers Get Excel column header configuration for a session
What It Does
Returns the custom Excel column headers configured for this session. These headers define what columns appear in the uploaded Excel file. Each header has a key, label, type (Score/Attendance/Comment), and optional max score. Use this to display the session's assessment structure.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min
Response — 200 OK
[ { "id": "h1-...", "key": "quiz1", "label": "Quiz 1", "labelArabic": "اختبار 1", "columnType": "Score", "maxScore": 20, "sortOrder": 1, "isRequired": false } ]
Property Reference
PropertyWhat It Means & How to Display
keyInternal identifier for this column. Used in Excel files and API calls. E.g. "quiz1"
labelDisplay name in English. Show as column header. E.g. "Quiz 1"
labelArabicDisplay name in Arabic. Show when the app language is set to Arabic.
columnTypeWhat kind of data this column holds: "Score" (numeric), "Attendance" (boolean), "Comment" (text). Determines the input widget.
maxScoreMaximum possible score for this column. Null for Attendance and Comment types. Show as "/20" next to the score input.
isRequiredWhether this column must be filled. Show as asterisk (*) next to the label if required.
PUT /api/v1/teachers/sessions/{sessionId}/headers Update Excel column headers for a session
What It Does
Replaces the entire header configuration for a session. The body is { "headers": [...] } with 1–8 entries, at least one Score column, at most 5 score columns, and at most 1 attendance / 1 comment column. Returns 409 Conflict if scores already exist for this session (to prevent breaking existing data). There is no copyHeadersFromSessionId field on this endpoint — fetch another session's headers and resend them.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min · Returns 409 if scores exist
GET /api/v1/teachers/sessions/{sessionId}/export-report Export session report as Excel file
What It Does
Generates and returns an Excel file containing the session report with all student scores, attendance, and comments. The file is downloaded as a binary stream. Use this for the "Export" button on the session detail screen.
Auth: Bearer token (teacher role, session owner) · Rate: 30 req/min · Returns: application/octet-stream
POST /api/v1/teachers/upload Upload an Excel score sheet
What It Does
Uploads an Excel file (.xlsx, .xls, or .csv) containing student scores. The file is processed asynchronously via Hangfire. Each row represents a student, and columns are the assessment headers. Returns immediately with an upload ID. The teacher can check processing status via the upload-logs endpoint. Max file size: 10MB. This is a multipart/form-data request, not JSON.
Auth: Bearer token (teacher role) · Rate: 5 req/min · Content-Type: multipart/form-data
Flutter Implementation
final request = http.MultipartRequest('POST', uri); request.headers['Authorization'] = 'Bearer $token'; request.fields['groupId'] = groupId; request.files.add(await http.MultipartFile.fromPath('file', filePath)); final response = await request.send();
Form Fields
FieldDescription
fileThe Excel file. Accepted formats: .xlsx, .xls, .csv. Max 10MB. Path traversal in filenames is rejected.
groupIdUUID of the group this upload is for. The teacher must own this group.
GET /api/v1/teachers/upload/template Download the Excel upload template
What It Does
Returns a pre-formatted Excel template that the teacher can fill in with student scores. The template has columns matching the session's header configuration. If sessionId is provided, the template includes custom headers for that session. Download this before uploading scores.
Auth: Bearer token (teacher role) · Rate: 5 req/min · Returns: application/octet-stream
Query Parameters
ParamDescription
groupIdUUID of the group (required). Determines student list in the template.
sessionIdUUID of the session (optional). If provided, enables session-bound mode with custom column headers.
GET /api/v1/teachers/upload-logs List upload history with processing status
What It Does
Returns a paginated list of all Excel uploads the teacher has made. Each entry shows the file name, processing status, row counts, and which group it was for. Use this for the "Upload History" screen. Poll this endpoint to check if a processing upload has completed. Pass ?uploadStatuses= to show only uploads in a given state (e.g. Failed).
Auth: Bearer token (teacher role) · Rate: 30 req/min
Query Parameters
ParamDescription
uploadStatusesRepeatable status filter: Queued, Processing, Completed, Failed. Omit to show all. Use it for "Failed only" views and status badge counts.
pagePage number, starting at 1. Default 1.
pageSizeItems per page. Default 20, max 100.
Property Reference
PropertyWhat It Means & How to Display
statusProcessing state: "Queued" (waiting), "Processing" (in progress), "Completed" (done), "Failed" (error). Show as colored badge: gray=Queued, yellow=Processing, green=Completed, red=Failed.
rawProcessedNumber of student rows successfully processed. Show as "30 students imported".
rawFailedNumber of rows that failed. If > 0, show warning: "2 rows failed". Tap to see error details.
uploadedAtWhen the file was uploaded. Show as relative time: "2 hours ago".
completedAtWhen processing finished. Null if still processing. Show as duration: "Processed in 5s".
GET /api/v1/teachers/upload-logs/{uploadLogId} Get details for a single upload
What It Does
Returns full details for a single upload including processing stats, error counts, and timing. Use this for the "Upload Detail" screen.
Auth: Bearer token (teacher role, upload owner) · Rate: 30 req/min
POST /api/v1/teachers/upload-logs/{uploadLogId}/retry Retry a failed upload
What It Does
Re-queues a failed upload for processing. Only works on uploads with "Failed" status. The original file is re-processed from scratch. Use this from the upload history screen when an upload fails.
Auth: Bearer token (teacher role, upload owner) · Rate: 30 req/min
GET /api/v1/teachers/groups/{groupId}/export-students Export student list as Excel
What It Does
Generates and returns an Excel file with all students in the group including their codes, scores, and attendance. Use this for the "Export" button on the group students screen.
Auth: Bearer token (teacher role, group owner) · Rate: 30 req/min · Returns: application/octet-stream
GET /api/v1/teachers/notifications List teacher's notifications
What It Does
Returns a paginated list of notifications for the teacher. Notifications are bilingual (English/Arabic) and include a type, title, body, and optional JSON payload with context data. Use this for the "Notifications" screen.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Property Reference
PropertyWhat It Means & How to Display
typeNotification category. Values: "NewGrade" (score published), "RankChange" (leaderboard position changed), "BadgeEarned" (achievement unlocked), "PaymentConfirmed" (payment verified), "SystemAlert" (platform announcement). Use to determine the icon and tap action.
isReadWhether the teacher has seen this notification. Show unread ones with bold title and blue dot indicator.
payloadJSON string with context data (e.g. sessionId, studentId). Parse this to determine what to open when the notification is tapped.
sentAtWhen the notification was sent. Show as relative time: "2 hours ago".
GET /api/v1/teachers/notifications/unread-count Get unread notification count
What It Does
Returns a single integer — the number of unread notifications. Poll this periodically (e.g. every 30 seconds) to update the badge on the notification bell icon. Returns 0 when all notifications are read.
Auth: Bearer token (teacher role) · Rate: 30 req/min
PUT /api/v1/teachers/notifications/{notificationId}/read Mark a single notification as read
What It Does
Marks one notification as read. Call this when the teacher taps on a notification or when it scrolls into view. After calling, decrement the unread count by 1.
Auth: Bearer token (teacher role) · Rate: 30 req/min
PUT /api/v1/teachers/notifications/read-all Mark all notifications as read
What It Does
Marks every notification as read in one call. Use this for a "Mark all as read" button. After calling, set unread count to 0 locally.
Auth: Bearer token (teacher role) · Rate: 30 req/min
POST /api/v1/teachers/notifications/send Send a notification to a student's parent
What It Does
Sends an in-app notification to the parent of a specific student. The teacher can choose the notification type (NewGrade, RankChange, etc.) and write a custom title and body. Use this for the "Notify Parent" action on student detail screens.
Auth: Bearer token (teacher role) · Rate: 30 req/min
Request Body
FieldDescription
studentIdUUID of the student (the notification goes to their parent)
titleNotification title. E.g. "Great Job, Sara!"
bodyNotification body. E.g. "Sara scored 95% on the Algebra midterm."
typeEnum: "NewGrade", "RankChange", "BadgeEarned", "PaymentConfirmed", "SystemAlert"
POST /api/v1/teachers/groups/{groupId}/notify-parents Bulk notify all parents in a group
What It Does
Sends a notification to the parents of every student enrolled in a group. Use this for class-wide announcements like "Mid-term results are ready" or "Next session is rescheduled". Each parent receives one notification per child in the group.
Auth: Bearer token (teacher role, group owner) · Rate: 30 req/min
Request Body
FieldDescription
titleNotification title. E.g. "Progress Update"
bodyNotification body. E.g. "Mid-term results for Math 101 are now available."
GET /api/v1/students/notifications List student's notifications
What It Does
Same structure as teacher notifications but for the student role. Returns notifications about grades, rank changes, badges, and system alerts. Use for the student notification screen.
Auth: Bearer token (student role) · Rate: 30 req/min
GET /api/v1/students/notifications/unread-count Student unread notification count
What It Does
Returns the number of unread notifications for the student. Poll periodically to update the notification badge.
Auth: Bearer token (student role) · Rate: 30 req/min
PUT /api/v1/students/notifications/{notificationId}/read Mark student notification as read
What It Does
Same as teacher version — marks a single notification as read for the student.
Auth: Bearer token (student role) · Rate: 30 req/min
GET /api/v1/teachers/groups/{groupId}/leaderboard Group leaderboard — full ranked array (not paginated)
What It Does
Returns every student in a group as a single LeaderboardEntryDto[] array — there is no page/pageSize and no items wrapper. Each entry includes the student's rank, scores, and contextual info like gap to top. The group is checked for existence only (404 if missing); the endpoint does not verify that the caller owns the group. Results are cached for 1 minute.
Auth: Bearer token (teacher role) · Rate: 30 req/min · Cached: 1 min
Query Parameters
ParamDescription
perioddaily | weekly | monthly | alltime (default weekly)
Property Reference
PropertyWhat It Means & How to Display
rankPosition on the leaderboard (1 = best). Display as rank badge: #1 gold, #2 silver, #3 bronze.
weightedScoreCombined score used for ranking. Weighted combination of avgScore, attendanceRate, and participationScore. Higher = better.
avgScoreAverage assessment score (0-100). The raw academic performance number.
attendanceRateWhat % of sessions the student attended (0-100). High attendance boosts ranking.
participationScoreScore based on session activity and engagement. Computed from session participation patterns.
context.totalStudentsTotal students on this leaderboard. Show as "out of 30" next to the rank.
context.scoreDifferenceToTopHow far behind the top student this student is. E.g. 5.2 means 5.2 points behind #1. Show as "Gap: -5.2" for motivation.
context.rankLabelHuman-readable tier: "Gold" (top 10%), "Silver" (top 25%), "Top 10%", "Developing". Display as colored badge.
context.isTopPerformerTrue if in the top 10%. Use to highlight top performers with special styling.
GET /api/v1/teachers/leaderboard Teacher-wide leaderboard across all groups
What It Does
Returns students across ALL the teacher's groups, ranked together. Unlike the group board, this endpoint is paginated: the response is { items, page, pageSize, totalCount }. Optional filters: period (default alltime), gradeLevel, page, pageSize (max 100). Use this for the "Overall Rankings" screen.
Auth: Bearer token (teacher role) · Rate: 30 req/min
GET /api/v1/students/leaderboard Full leaderboard for one of the student's groups
What It Does
Returns the full paginated board for one group, not just the caller's row. groupId defaults to the student's primary active group. A group the student never belonged to returns 403; a group with no computed board returns 200 with an empty page. Use this for the "My Rankings" screen.
Auth: Bearer token (student role) · Rate: 30 req/min
Query Parameters
ParamDescription
groupIdGroup to show (optional; defaults to the primary active group)
perioddaily | weekly | monthly | alltime (default alltime)
page / pageSizePaging for the full board (default 1 / 20, max 100)
Property Reference
FieldDisplay guidance
items[].rankRank badge: #1 gold, #2 silver, #3 bronze, rest gray
items[].attendanceRate0–100 (already multiplied) — never treat as a fraction
items[].contextUse isTopPerformer for a highlight row
GET /api/v1/students/home Complete student dashboard — one call for the entire home screen
What It Does
Returns everything the student needs for their home screen. This is the student equivalent of /teachers/home — a single call that powers the entire dashboard. Includes profile, stats, enrolled groups, leaderboard rankings, recent sessions, badges, subject analysis, and class position. Cached for 2 minutes.
Auth: Bearer token (student role) · Rate: 30 req/min · Cached: 2 min
Response — 200 OK
{ "profile": { "fullName": "Sara Ali", "email": "sara@student.com", "studentCode": "STU001" }, "overview": { "totalGroups": 3, "totalSessions": 36, "overallAttendancePercentage": 92.5, "overallAverageScore": 85.3, "totalBadges": 5, "currentRank": 3, "scoreTrend": "up", "attendanceStreak": 8, "bestSubject": "Mathematics", "weakestSubject": "Science", "improvementAreas": ["Improve Science scores", "Increase attendance"], "predictedFinalGrade": "A" }, "enrolledGroups": [ { "groupId": "a1b2c3d4-...", "name": "Math 101 - A", "subject": "Mathematics", "teacherName": "Ahmed Hassan", "gradeLevel": "SecondaryOne", "period": "First Term" } ], "rankings": [ { "groupId": "a1b2c3d4-...", "groupName": "Math 101 - A", "rank": 3, "weightedScore": 87.5, "totalStudents": 30 } ], "recentSessions": [ { "sessionId": "s1-...", "groupName": "Math 101 - A", "sessionDate": "2025-09-28T00:00:00Z", "sessionType": "Exam", "attended": true, "rawScore": 19, "maxScore": 20, "scorePercentage": 95, "comment": "Excellent work" } ], "recentBadges": [ { "badgeType": "StudentOfWeek", "awardedAt": "2025-09-28T00:00:00Z" } ], "unreadNotifications": 2, "subjectAnalysis": { "subjects": [ { "subjectName": "Mathematics", "averageScore": 90.0, "sessionCount": 12 }, { "subjectName": "Science", "averageScore": 75.0, "sessionCount": 8 } ], "bestSubject": "Mathematics", "weakestSubject": "Science" }, "classPosition": { "rank": 3, "totalStudents": 30, "percentile": 90.0, "groupName": "Math 101 - A", "groupId": "a1b2c3d4-..." } }
Property Reference
PropertyWhat It Means & How to Display
profile — Student identity
profile.studentCodeUnique code the teacher uses to enroll this student. Show in profile header: "Code: STU001". Students can share this code with teachers.
overview — Key metrics
overallAverageScoreAverage score across ALL groups (0-100). The student's most important number. Display as large circular progress indicator. Color: green ≥80, yellow 60-79, red <60.
overallAttendancePercentageWhat % of all sessions the student attended. Show as percentage bar. Below 70% = red warning.
totalBadgesTotal achievement badges earned. Show with trophy icon: "5 badges earned".
currentRankBest rank across all enrolled groups. 0 if not ranked. Show as "Rank #3 in your best group".
scoreTrendDirection of scores. Values: "up", "down", "stable". Computed by splitting scores in half and comparing recent vs older half. Show as arrow icon.
attendanceStreakConsecutive sessions attended. Gamification metric. Show with fire icon: "8-session streak!"
bestSubjectSubject with the highest average score. E.g. "Mathematics". Show as highlight: "Your best: Mathematics"
weakestSubjectSubject with the lowest average score. E.g. "Science". Show as improvement suggestion: "Focus on: Science"
improvementAreasAuto-generated suggestions: "Improve Science scores", "Increase attendance", "Focus on exams". Show as actionable tips list.
predictedFinalGradePredicted grade based on current average. Values: "A" (≥90), "B+" (≥80), "B" (≥70), "C" (≥60), "D" (≥50), "F" (<50). Show as goal indicator.
subjectAnalysis — Per-subject breakdown
subjectAnalysis.subjectsList of subjects with average scores and session counts. Display as bar chart or radar chart. Each bar shows subject name and score.
subjectAnalysis.bestSubjectSame as overview.bestSubject. Reinforces strength.
subjectAnalysis.weakestSubjectSame as overview.weakestSubject. Reinforces improvement area.
classPosition — Leaderboard position
classPosition.rankStudent's rank in their best-performing group. Show as "You're #3 out of 30 in Math 101 - A".
classPosition.percentilePercentile position (0-100). 90 means better than 90% of the class. Show as "Top 10%".
Other fields
enrolledGroupsList of groups the student belongs to. Show as group cards with teacher name and subject.
rankingsLeaderboard position in each enrolled group. Show as rank badges on group cards.
recentSessionsLast 5 sessions with attendance and scores. Show as a timeline list. Each item shows group, date, type, score, and attended status.
recentBadgesLast 3 badges earned. Show as horizontal scroll of badge icons with names and dates.
GET /api/v1/students/sessions Student session history (paginated, no summary object)
What It Does
Returns the student's session history inside each membership window as a PaginatedResult<StudentSessionResponseDto>. A recorded attendance session with no student row counts as absent; sessions where nobody recorded attendance are excluded. Each row includes raw score, max score, percentage, and every assessment for that session. There is no summary object — compute totals client-side or read them from /students/home.
Auth: Bearer token (student role) · Rate: 30 req/min
Response — 200 OK
{ "items": [ { "sessionId": "s1-...", "groupName": "Math 101 - A", "sessionDate": "2025-09-28T00:00:00Z", "sessionType": "Exam", "attended": true, "score": 19, "maxScore": 20, "scorePercentage": 95.0, "comment": "Excellent", "groupId": "g1-...", "assessments": [ { "assessmentId": "a1-...", "examName": "Unit 3 Written", "topic": "Mitosis", "kind": "Written", "rawScore": 19, "maxScore": 20, "scorePercentage": 95, "teacherComment": "Excellent" } ], "studentId": null, "studentName": null, "studentCode": null } ], "page": 1, "pageSize": 20, "totalCount": 36 }
Property Reference
PropertyWhat It Means & How to Display
page / pageSize / totalCountPagination envelope (default 1 / 20, max 100). Build the pager from totalCount.
items[].attendedAttendance for this session. Missing row in a recorded session is returned as false.
items[].score / maxScoreRaw score of the best assessment in the session — render as 19/20. Null when ungraded.
items[].assessments[]All assessments for that session; render one card per assessment with kind badge and rawScore/maxScore.
items[].scorePercentage0–100. Colored number; null when ungraded.
items[].studentId / studentName / studentCodeNull on the student endpoint; populated on the teacher-facing list.
GET /api/v1/students/sessions/{sessionId} Single session detail for a student
What It Does
Returns the student's specific view of a session — their attendance status, scores, and teacher comments. Does not show other students' data. Use this for the "Session Detail" screen from the student side.
Auth: Bearer token (student role) · Rate: 30 req/min
GET /api/v1/students/badges Complete badge catalog with the student's earned state
What It Does
Returns the entire fixed badge catalog, each entry annotated with isEarned and awardedAt, plus overall progress. The catalog is fixed, so this endpoint is never paginated — render it as a grid and grey out unearned badges.
Auth: Bearer token (student role) · Rate: 30 req/min
Response — 200 OK
{ "badges": [ { "badgeId": "...", "badgeType": "StudentOfWeek", "metaData": "{\"description\":\"Top performer of the week\",\"icon\":\"star\"}", "isEarned": true, "awardedAt": "2026-09-10T00:00:00Z" }, { "badgeId": "...", "badgeType": "HighestScore", "metaData": "{\"description\":\"Achieved the highest score\",\"icon\":\"crown\"}", "isEarned": false, "awardedAt": null } ], "progress": { "totalBadgesEarned": 4, "totalBadgesAvailable": 7, "completionPercentage": 57.1, "recentAchievements": [], "nextBadgeToEarn": { "badgeType": "HighestScore", "badgeName": "Highest Score", "isEarned": false, "awardedAt": null, "description": "Achieved the highest score" } } }
Property Reference
FieldDisplay guidance
badges[].isEarnedEarned → full color + date; not earned → 40% opacity lock icon
badges[].metaDataArabic/English metadata label (previously metadata — update the model)
badges[].awardedAtRelative date, null when unearned
progress.completionPercentage0–100 ring at the top of the grid
progress.nextBadgeToEarnCall-to-action card; null when all badges are earned
GET /api/v1/students/profile Get the authenticated student's profile
What It Does
Returns the student's profile including their unique student code. This code is what teachers use to enroll the student into groups. Show it prominently so students can share it with their teachers.
Auth: Bearer token (student role) · Rate: 30 req/min
PUT /api/v1/students/profile Update the authenticated student's profile
What It Does
Updates the student's profile fields (fullName, phoneNumber, email). Same behavior as teacher profile update — partial update, email change triggers re-verification.
Auth: Bearer token (student role) · Rate: 30 req/min
GET /api/v1/students/groups List groups the student is enrolled in
What It Does
Returns the student's group memberships, including enrollmentStatus, approvedAt, removedAt, and per-group stats. Removed memberships still appear so the student can open past work; only pending/active memberships appear in live rosters and new sessions.
Auth: Bearer token (student role) · Rate: 30 req/min
GET /api/v1/parents/profile Get the authenticated parent's profile
What It Does
Returns the parent's profile including subscription plan, consent flag, and contact details. Same update pattern as teacher/student profiles.
Auth: Bearer token (parent role) · Rate: 30 req/min
PUT /api/v1/parents/profile Update the authenticated parent's profile
What It Does
Partial update of the parent's fullName and phoneNumber. Omitted fields stay unchanged.
Auth: Bearer token (parent role) · Rate: 30 req/min
GET /api/v1/parents/home Complete parent dashboard — one call for the home screen
What It Does
Returns everything the parent needs: their profile, aggregate stats across all children, individual child summaries (with scores, attendance, ranks, badges), recent payments, and notification count. This is the parent equivalent of the teacher/student home endpoints.
Auth: Bearer token (parent role) · Rate: 30 req/min
Response — 200 OK
{ "profile": { "fullName": "Mohamed Ali", "email": "mohamed@parent.com", "subscriptionPlan": "Premium" }, "overview": { "totalChildren": 2, "averageAttendance": 88.5, "averageScore": 82.3, "totalPaidEGP": 15000.00, "pendingPaymentsEGP": 3500.00 }, "children": [ { "studentId": "s1t2u3d4-...", "fullName": "Sara Ali", "studentCode": "STU001", "groupsEnrolled": 3, "attendancePercentage": 92.5, "averageScore": 85.3, "totalSessions": 36, "totalBadges": 5, "currentRank": 3, "scoreTrend": "up", "bestSubject": "Mathematics" } ], "recentPayments": [ { "paymentId": "p1-...", "studentName": "Sara Ali", "amountEGP": 1500.00, "periodMonth": "2025-09-01T00:00:00Z", "status": "Confirmed", "confirmedByTeacher": true } ], "unreadNotifications": 1 }
Property Reference
PropertyWhat It Means & How to Display
overview.totalChildrenNumber of children linked to this parent account. Show as stat: "2 children"
overview.averageAttendanceAverage attendance across all children (0-100). Show as aggregate stat card.
overview.averageScoreAverage score across all children (0-100). The parent's overall view of academic performance.
overview.totalPaidEGPTotal confirmed payments in Egyptian Pounds. Show as "15,000 EGP paid".
overview.pendingPaymentsEGPTotal pending payments. Show with warning color if > 0: "3,500 EGP pending".
children.*.attendancePercentageThis child's attendance rate (0-100). Show as percentage on the child card.
children.*.averageScoreThis child's average score (0-100). Show as progress bar on the child card.
children.*.currentRankBest rank across groups. Null if not ranked. Show as "Rank #3".
children.*.scoreTrendScore direction: "up", "down", "stable". Show as arrow icon on child card.
children.*.bestSubjectSubject with highest score. Show as "Best: Mathematics".
recentPaymentsLast 3 payment records. Show as payment history list with status badges.
recentPayments.*.status"Pending", "Confirmed", or "Disputed". Show as colored badge.
GET /api/v1/parents/children List all children
What It Does
Returns every child linked to the parent's account, each with group memberships (enrollmentStatus) and stats computed in SQL. Attendance counts only recorded attendance sessions — a missing row counts as absent. Use this for the "My Children" screen.
Auth: Bearer token (parent role) · Rate: 30 req/min
Property Reference
FieldDisplay guidance
groups[].enrollmentStatusPending amber, Active green, Removed grey — removed groups stay visible for history
stats.attendanceRate0–100; flag <70% amber, <60% red
stats.scoreTrend"up" / "down" / "stable" arrow next to the average
stats.bestSubjectHighlight chip on the child card
GET /api/v1/parents/children/{studentId}/progress Get detailed progress for a specific child
What It Does
Returns aggregate progress for one child: overall attendance, overall average score, session count, and assessment count. The student must be linked to the calling parent, otherwise 403.
Auth: Bearer token (parent role) · Rate: 30 req/min
GET /api/v1/parents/children/{studentId}/sessions Get session history for a specific child
What It Does
Returns the child's sessions inside each membership window with attendance, top-level score, and an assessments[] array (raw score, max score, percentage). A recorded attendance session with no student row counts as absent. Same data the student sees, scoped to this parent.
Auth: Bearer token (parent role) · Rate: 30 req/min
GET /api/v1/parents/children/{studentId}/leaderboard Get leaderboard position for a specific child
What It Does
Returns the full paginated board for the child's primary active group (or ?groupId=). A group the child never belonged to returns 403; no computed board returns 200 with an empty page. attendanceRate is 0–100. Use this for the "Child Rankings" screen.
Auth: Bearer token (parent role) · Rate: 30 req/min
POST /api/v1/payments Create a payment record (parent-initiated)
What It Does
Creates a new payment record. The parent initiates this to pay for their child's tutoring. The payment starts in "Pending" status until the teacher confirms it. Amount is in Egyptian Pounds (EGP). Use this from the "Make Payment" screen.
Auth: Bearer token (parent role) · Rate: 20 req/min
Request Body
FieldDescription
teacherIdUUID of the teacher being paid (required)
studentIdUUID of the student this payment is for (required)
amountEGPPayment amount in Egyptian Pounds, > 0 (required). E.g. 1500.00
periodMonthMonth this payment covers. E.g. "2025-09-01". Null if not period-specific.
notesOptional description. E.g. "September tutoring fee"
GET /api/v1/payments List payments for the parent (paginated)
What It Does
Returns the parent's payments as { payments: PaginatedResult<PaymentResponseDto>, summary: PaymentSummaryDto }. The list is nested under payments.items — there is no top-level items. pageSize defaults to 10 (max 100). summary holds aggregate totals and a monthly breakdown for trend charts. Use this for the "Payment History" screen.
Auth: Bearer token (parent role) · Rate: 20 req/min
Response — 200 OK
{ "payments": { "items": [ { "paymentId": "p1-...", "teacherId": "t1-...", "teacherName": "Ahmed Hassan", "studentId": "s1-...", "studentName": "Sara Ali", "amountEGP": 1500.00, "periodMonth": "2025-09-01T00:00:00Z", "status": "Confirmed", "confirmedByTeacher": true, "notes": "September fee", "createdAt": "2025-09-01T10:00:00Z" } ], "page": 1, "pageSize": 10, "totalCount": 12 }, "summary": { "totalPaidEGP": 15000.00, "totalPendingEGP": 3500.00, "totalDisputedEGP": 0.00, "totalAmountEGP": 18500.00, "totalPayments": 12, "paidCount": 9, "pendingCount": 2, "disputedCount": 1, "monthlyBreakdown": [ { "year": 2025, "month": 9, "paidEGP": 3000.00, "pendingEGP": 1500.00, "paymentCount": 3 } ] } }
GET /api/v1/payments/teacher List payments for the teacher (paginated)
What It Does
Returns payments received by the teacher in the same { payments: PaginatedResult<PaymentResponseDto>, summary: PaymentSummaryDto } envelope. pageSize defaults to 10 (max 100). Shows which students/parents have paid, pending, or disputed. Use this for the teacher's "Payments" screen.
Auth: Bearer token (teacher role) · Rate: 20 req/min
PUT /api/v1/payments/{paymentId}/confirm Confirm or dispute a payment (teacher action)
What It Does
Teacher confirms they received the payment (sets status to "Confirmed") or disputes it (sets to "Disputed"). After confirmation, the parent sees a green checkmark. After dispute, the parent sees a red alert and can contact the teacher. Only the teacher who received the payment can confirm/dispute.
Auth: Bearer token (teacher role, payment recipient) · Rate: 20 req/min
Request Body
FieldDescription
confirmedtrue = confirm receipt, false = dispute the payment
notesOptional note. E.g. "Received cash" or "Amount doesn't match agreed fee"
GET /api/v1/admin/dashboard Platform-wide admin dashboard stats
What It Does
Returns comprehensive platform statistics: user counts by role, revenue metrics, registration trends, top performers, and recent activity. This powers the admin dashboard. Only accessible to users with the Admin role.
Auth: Bearer token (admin role) · Rate: 30 req/min
Property Reference
PropertyWhat It Means & How to Display
totalUsersTotal registered users across all roles. Show as main stat card.
totalTeachers / totalStudents / totalParentsUser counts by role. Show as breakdown cards or pie chart.
activeUsers / inactiveUsersActive vs deactivated accounts. Show as toggle stat or bar chart.
verifiedUsers / unVerifiedUsersEmail verification status. High unverified count may indicate onboarding issues.
usersThisWeekNew registrations in the last 7 days. Show as growth metric.
growthPercentWeek-over-week registration growth. Show as "+12.5%" with arrow.
registrationTrendDaily registration counts for the last 30 days. Show as line chart.
totalRevenueEGPTotal confirmed revenue in EGP. Show as main revenue card.
pendingRevenueEGPRevenue awaiting teacher confirmation. Show as secondary card.
platformAvgScoreAverage score across all sessions platform-wide. Show as performance indicator.
platformAttendanceRateAverage attendance rate platform-wide. Show as health metric.
recentUploadsLast 5 file uploads across all teachers. Show as activity feed.
topStudentsTop 5 students by score. Show as leaderboard card.
topTeachersTop 5 teachers by student scores. Show as teacher rankings.
GET /api/v1/admin/groups/stats All groups with statistics
What It Does
Returns stats for every group on the platform. Use for the admin "Groups" management screen.
Auth: Bearer token (admin role) · Rate: 30 req/min
GET /api/v1/admin/users List all users (paginated, filterable)
What It Does
Returns a paginated list of all users. Filter by role. Use for the admin "Users" management screen.
Auth: Bearer token (admin role) · Rate: 30 req/min
GET /api/v1/admin/users/{userId} Get single user details
What It Does
Returns full details for a single user. Use for the admin "User Detail" screen.
Auth: Bearer token (admin role) · Rate: 30 req/min
PUT /api/v1/admin/users/{userId}/toggle-active Toggle user active/inactive status
What It Does
Activates or deactivates a user account. Deactivated users cannot log in. Use with confirmation dialog in the admin panel.
Auth: Bearer token (admin role) · Rate: 30 req/min
GET /api/v1/admin/groups/{groupId} Get single group details (admin view)
What It Does
Returns full details for any group on the platform. Admins can view any teacher's groups. Use for the admin "Group Detail" screen.
Auth: Bearer token (admin role) · Rate: 30 req/min
GET /api/v1/admin/metrics Platform performance metrics
What It Does
Returns a subset of admin dashboard stats focused on platform performance. Use for the admin "Metrics" or "Analytics" screen.
Auth: Bearer token (admin role) · Rate: 30 req/min
GET /api/v1/subjects List all available subjects
What It Does
Returns a paginated list of subjects in the { items, page, pageSize, totalCount } envelope. Each subject has English and Arabic names. Use this to populate subject dropdowns when creating or editing groups. Teachers and admins can see all subjects. Pass ?searchName= to filter by name (case-insensitive contains); blank or omitted means no filtering.
Auth: Bearer token (any role) · Rate: 30 req/min
Response — 200 OK
{ "items": [ { "id": "c3d4e5f6-...", "name": "Mathematics", "nameArabic": "الرياضيات", "isActive": true, "createdAt": "2024-01-01T00:00:00Z", "updatedAt": null } ], "page": 1, "pageSize": 20, "totalCount": 1 }
Query Parameters
ParamDescription
searchNameCase-insensitive contains filter on the English name. Omit or blank for all. Use it for searchable subject dropdowns.
includeInactiveAdmins only: true includes deactivated subjects. Ignored for non-admin roles.
pagePage number, starting at 1. Default 1.
pageSizeItems per page. Default 20, max 100.
Property Reference
PropertyWhat It Means & How to Display
idSubject UUID. Use when creating a group (subjectId field in CreateGroupRequestDto).
nameEnglish name. E.g. "Mathematics". Show in English-language UI.
nameArabicArabic name. E.g. "الرياضيات". Show when app language is set to Arabic.
isActiveWhether the subject is available. Inactive subjects are hidden from dropdowns.
GET /api/v1/subjects/{subjectId} Get a single subject by ID
What It Does
Returns full details for a single subject. Use when you need the subject's Arabic name or verification status.
Auth: Bearer token (any role) · Rate: 30 req/min
POST /api/v1/subjects Create a new subject (admin only)
What It Does
Creates a new subject with both English and Arabic names. Only admins can create subjects. Teachers then use the subject ID when creating groups.
Auth: Bearer token (admin role) · Rate: 30 req/min
Request Body
FieldDescription
nameEnglish name, max 100 characters. E.g. "Mathematics"
nameArabicArabic name, max 100 characters. E.g. "الرياضيات"
PUT /api/v1/subjects/{subjectId} Update a subject (admin only)
What It Does
Updates a subject's names or active status. Only admins can update subjects. Deactivating a subject hides it from dropdowns but doesn't affect existing groups that use it.
Auth: Bearer token (admin role) · Rate: 30 req/min
DELETE /api/v1/subjects/{subjectId} Deactivate a subject (admin only)
What It Does
Soft-deletes a subject by setting isActive = false. The subject is not removed from the database. Existing groups that use this subject continue to work. New groups cannot be created with this subject until it's reactivated.
Auth: Bearer token (admin role) · Rate: 30 req/min
POST /api/v1/dev/quick-login Mint a JWT for a seeded user (development only)
What It Does
Returns a JWT access token for the first active seeded user with the requested role. Query parameter role accepts teacher (default), student, parent, admin. Registered only in Development and returns 404 elsewhere. Requires the ComprehensiveSeeder to have run.
Auth: None · Rate: 10 req/min · Development only
Response — 200 OK
{ "accessToken": "eyJ...", "role": "Teacher", "userId": "b0000000-...", "email": "teacher1@track2improve.com" }
POST /api/v1/dev/reseed-notifications Rebuild demo notifications with EN/AR text (development only)
What It Does
Deletes all notifications and re-seeds them with bilingual English + Arabic content. Returns the number deleted and inserted. Development only — 404 outside Development.
Auth: None · Rate: 10 req/min · Development only
Response — 200 OK
{ "message": "Deleted 42 old notifications and inserted 48 new ones with bilingual text.", "inserted": 48 }
GET /health Liveness probe
What It Does
Returns 200 with the plain-text body Healthy whenever the process is running. No authentication and no database check. Use it for uptime monitors and container liveness probes.
Auth: None · Rate: none
GET /health/ready Readiness probe (database + Hangfire)
What It Does
Runs the ready-tagged checks (database connectivity and Hangfire server availability). Returns 200 when healthy and 503 otherwise, with a JSON status payload.
Auth: None · Rate: none
Response — 200 OK
{ "status": "Healthy", "checks": [ { "name": "AppDbContext", "status": "Healthy", "exception": null, "duration": "00:00:00.0080000" } ] }