# Student Outcome Profile

Search a student ID and see how that student performs on every Program Outcome (PO) across all completed courses: a radar against the same-course average and the pass line, a course × PO heatmap, and a cumulative curve term by term.

Open it from the sidebar (**Outcomes → Student Profile**) or link to `#/students/<studentId>`.

**Download PDF** gives a report ready to print or share. It includes:
- the institution header and student details;
- key figures;
- the radar with written findings: strengths, POs needing attention, comparison with classmates, and trend;
- the PO summary table;
- a colour-coded course × PO table;
- a small progress chart per PO;
- the course list and a note on how to read the report.

**Download Excel** gives the same figures in six sheets: Summary, PO Profile, Course x PO, Weights, Courses and Cumulative. Both files are built in the browser from the same data as the page, so they always match it. On a course's **Attainment** tab, the new *Student-wise CO & PO Scores* table links every student ID to their profile.

## How the numbers are made

| Step | Rule |
|---|---|
| Student CO % | Marks on the counted items of that CO ÷ their maximum × 100. Counting rules R1–R5 (below). With the **assessment-weighted** basis, every mark and maximum is first multiplied by the assessment's weight ÷ its full marks. Full marks = mandatory questions + the k largest choice units. |
| Student PO % (one course) | Σ w·CO% ÷ Σ w over the COs the student was assessed on. With **CO–PO strength** weighting, w = strength (1/2/3). With **normalised per CO** (legacy), w = strength ÷ that CO's total strength across all POs. The legacy formula reproduces the original OBE Calculation Aid (e.g. CE 207, student 2004001: PO1 = 46.84). |
| CO / PO attainment (class) | % of assessed students scoring **above** Target Pass Marks (default 40). Attained when that reaches the Target KPI (default 50 %). |
| Course weight for a PO | credit × summed CO-PO strength of that PO in the course. |
| Profile PO % | Weighted mean of the student's per-course PO % using the course weights. A retaken course code counts once, with its best score per PO. |
| Same-course average | Average PO % of everyone in the same course offerings, weighted the same way. |
| Cumulative curve | The profile recomputed with the courses of each term added in order. |
| Radar | One axis per program PO (PO1–PO12) in a fixed order, scale 0–100. A PO without evidence is "n/a" (a gap, not 0). A PO assessed by only one course has a hollow point and counts as provisional. |

### Calculation methods

Each course stores two settings in `cfg` (Course Overview → Attainment Thresholds):

| Setting | Standard | Legacy (value when unset) |
|---|---|---|
| `coBasis` | `weighted`: assessments count with their declared weight | `raw`: raw marks are pooled |
| `poWeighting` | `strength`: w = CO–PO strength | `normalised`: w = strength ÷ the CO's row total |

New courses take the institution defaults (Setup → Attainment Defaults), which are the standard methods. A course saved before these settings existed keeps the legacy methods, so its results and saved profile scores do not change. If `coBasis` is `weighted` but an assessment with CO-mapped questions has no positive weight, the engine falls back to raw marks and reports `coBasisApplied: "raw"`. After switching a completed course's method, run `php bin/rebuild-student-outcomes.php` to re-freeze its scores.

Counting rules (blank = no mark entered; 0 = attempted and scored zero):

- **R1** an attempted item counts.
- **R2** a blank mandatory item counts as 0.
- **R3** a blank optional item counts as 0 when the student attempted another item of the same group.
- **R4** a blank optional item whose group was untouched is excluded.
- **R5** with "answer k of n" (`chooseK`), missing units are added as zeros, highest maximum first; a student with no mark anywhere in that assessment is treated as absent from it.

A student with nothing counted under a CO is left out of that CO (shown as "—").

Mark questions as **Optional** in the Assessments tab. Give the sub-parts of one choice question the same **Group**, and set **Answer any k** on the assessment.

> The full methodology for faculty, including the CO exit survey and combined attainment, is `docs/OBEsoft_Attainment_Methodology.docx`. `docs/Attainment_Calculation_Methodology.docx` is preserved unchanged: it describes the standalone v2.19 edition, which weights POs by CO allocated marks.

The rules are implemented twice, and both implementations must stay identical:

- `public/assets/js/attainment-engine.js`: the browser, for the Attainment tab and the other tabs.
- `src/Services/AttainmentService.php`: the server, for the scores saved at completion.

`tests/fixtures/attainment_cases.json` holds hand-checked cases that both implementations must pass.

## When scores are saved

- **Complete a course** (Course Completion tab): every student's PO % for that course is saved to `student_po_scores`, in the same transaction as the status change.
- **Reopen**: that course's scores are removed until it is completed again.
- **Delete a course**: its scores are deleted with it.
- Marks are never read live by the profile, so a course that is still in progress does not affect anyone's profile.
- A student with no mark at all in a course is treated as absent and gets no scores from it (the importer applies the same rule).
- Completed courses that have no saved scores are filled in automatically the next time the profile page is used. This covers courses completed before upgrading and databases restored from a backup.

## Who can see what

| Role | Profiles visible |
|---|---|
| Admin, Manager | Every student |
| Teacher | Students who took one of the teacher's assigned courses (their full profile) |

Permission: `students.read` (all three roles). Imported legacy rows have no course, so only admins and managers see students who exist only in imported data.

## Data

Migration `010_student_outcomes.php` adds:

- `students`: `id` (the roll number, trimmed), `name`.
- `student_po_scores`: one row per student × course offering × PO.
  - Columns: `score`, `weight`, `credit`, `term_label`, `term_order`, `source` (`course` or `import`).
  - `offering_key` is the course ID for completed courses, or `import:<code>:<term>` for imports.

Terms sort by year, then part of the year: January/Spring, then July/Summer, then Fall. Labels such as `L-2 T-1` sort by level and term.

## API

```text
GET api/student-profile.php?q=<text>   search by ID or name (plus overview counts)
GET api/student-profile.php?id=<id>    full profile; 404 when there is no visible data
```

## Command-line tools

Re-freeze every completed course, for example after changing the attainment rules. Courses that are missing scores are filled in automatically, so this is only needed when existing scores should be recomputed:

```text
php bin/rebuild-student-outcomes.php
```

Import the old OBE Calculation Aid (Colab) reports, one term folder at a time. Each course sub-folder needs `Report <CODE>.xlsx` (sheet *PO report*) and `CO_PO Mapping.xlsx`:

```text
php bin/import-legacy-reports.php --dir="D:/OBE Softwere/Inputs/db_Term Jan23" --semester=January --year=2023 --dry-run
php bin/import-legacy-reports.php --dir="D:/OBE Softwere/Inputs/db_Term Jan23" --semester=January --year=2023
```

- `--credit=3` sets the credit used for every course in the folder.
- Re-running the same term replaces that term's imported rows.
- The old reports counted every blank mark as 0, so imported scores follow the old rules.
- Rows whose PO scores are all 0 (absent students) are skipped.

## Tests

- `node tests/attainment_engine_test.js`: the browser engine against the shared fixture.
- `php tests/attainment_parity_test.php`: 400 random, deliberately messy courses through both engines; every number must match.
- `php tests/student_outcomes_regression.php`: the PHP engine against the same fixture, plus:
  - snapshots, retakes, cohort average and trajectory;
  - access scoping, reopen and delete;
  - the `.xlsx` importer.
- `tests/http_smoke.ps1` and `tests/auth_http_smoke.ps1`:
  - completion → profile → reopen through the API;
  - teachers cannot see students of unassigned courses.
