# Classly — QA Review & Fixes

**Date:** 2026-08-27
**Scope:** Full web app + REST API, tested as Super Admin, School Admin, Teacher, Accountant, Receptionist, Librarian, Parent, Student.
**Environment:** local (WAMP, MySQL 8.4, Laravel 12.65, PHP 8.3), DB `smart_school`, seeded demo data.

Login for all demo accounts: password `password`.

---

## Status

| # | Severity | Issue | Status |
|---|----------|-------|--------|
| 1 | Critical | Topbar notifications + messages broken on every page | **Fixed** |
| 2 | Critical | `/library/members` returns HTTP 500 | **Fixed** |
| 3 | High | Teachers cannot use Homework / Lesson Plans / Live Classes (web) | **Fixed** |
| 4 | High | Tenant isolation bypass for users with `school_id = NULL` | **Fixed** |
| — | High | Super admin "enter school" did not actually scope data | **Fixed** |
| 5 | Medium | Demo/seed data incomplete (fees, submissions, teacher staff record) | **Fixed** |
| 6 | Medium | No "Forgot Password" / password-reset flow | **Fixed** |
| 7 | Medium | Parent portal page reachable by non-parents | **Fixed** |
| 8 | Medium | No teacher scoping on attendance / marks entry | Open (design decision) |
| 9 | Low | Attendance status values not validated | **Fixed** |
| 10 | Low | "Change Password" menu link had no anchor | **Fixed** |
| 11 | Low | `APP_DEBUG=true` in local `.env` | Note only (set false in production) |
| 12 | Low | Parent dashboard shows guardian name redundantly | Not changed (cosmetic, correct data) |
| 13 | Low | `hostel/assign` null-guard | **Fixed** |
| — | — | Mobile app: teachers cannot log in | Open (product decision — see note) |

---

## Fixes applied

### 1. Notifications & Messages dropdowns — CRITICAL ✅
`resources/views/layouts/partials/topbar.blade.php` read `ES_DATA.dropdown` instead of
`ES_DATA.dropdownUrl` → `fetch(undefined)` → `GET /undefined` 404 on **every page for
every user**. Fixed the variable name. Verified `/notifications/dropdown` now returns 200
and the badges/lists render.

### 2. `/library/members` HTTP 500 — CRITICAL ✅
`LibraryController@members()` ordered `Student` by a non-existent `name` column and
`Staff` by a non-existent `first_name` column (staff names live on the linked `user`).
Now orders students by `first_name, last_name` and staff by `user.name`. Verified 200 for
admin and librarian.

### 3. Teacher access to Homework / Lesson Plans / Live Classes — HIGH ✅
- Added `manage-academics` permission (`routes: ['academic.*']`) to
  `config/role_permissions.php` and granted it to the `teacher` role by default.
- `RolePermissionSeeder` now flushes the permission cache after syncing, so re-seeding
  takes effect immediately (the effective-permissions lookup caches for 1 hour).
- Verified: teacher now gets 200 on all three `/academic/*` pages.

**Mobile note:** `app/Http/Controllers/Api/AuthController@login` still only allows
`student`, `parent`, `super-admin` on the mobile app. If teachers should manage academics
from their phone, add `teacher` to that list — left as a product decision because the
Flutter screens are currently student/parent-oriented.

### 4. Tenant isolation for `school_id = NULL` users — HIGH ✅
`app/Models/Concerns/BelongsToTenant.php` + `app/Models/Tenant.php`:
- A staff-type user with no resolvable school now sees **nothing** (`whereRaw('1 = 0')`)
  instead of every school's data.
- Portal roles (parent/student) are exempt — they reach data through ownership checks
  (`$parent->children`), so a multi-school parent keeps working.
- `Tenant::moduleEnabled()` now **denies** a school-less non-super-admin instead of
  allowing everything.
- Seed data no longer produces such users (see #5).

### Super admin "Enter school" (log in as that school's admin) — HIGH ✅
Previously the button set a session flag but the super admin still bypassed all tenant
scoping, so "entering" a school showed global data.
- New `Tenant::isImpersonating()` / `Tenant::hasGlobalAccess()`. While a super admin has
  an active school context, they are scoped to that school and subject to its module
  gating — i.e. they act as that school's admin.
- `resources/views/schools/index.blade.php`: the school **name is now a click target**
  that logs you in to that school; the row action is relabelled "Log in as admin".
- A dark banner appears on every page while impersonating —
  *"You are viewing &lt;School&gt; as its administrator"* — with an **Exit to school
  list** button (`resources/views/layouts/app.blade.php`).
- Verified: enter → dashboard shows the banner and school-scoped counts; `/schools`
  stays reachable to switch/exit; exit clears the context.

### 5. Demo / seed data completeness — MEDIUM ✅
- New `database/seeders/DemoNormalizationSeeder` (runs last):
  - assigns every staff-type account with no school to the primary demo school;
  - runs `schools:backfill-tenant-ids` (twice) to derive `school_id` everywhere;
  - seeds **fee payments** for ~70% of fee assignments (some partial, ~30% left unpaid so
    the defaulter list is also populated);
  - seeds **homework submissions** (some graded, ~1 in 3 left pending).
- `database/seeders/RoleScenarioSeeder` now gives `teacher@classlypk.com` a school, a
  `staff` record **and** two class-subject assignments, and sets the school on
  `student@classlypk.com` — so the Teacher and Student dashboards have real content.
- `app/Console/Commands/BackfillTenantIds` extended to derive `teacher_class_subjects`
  and `staff` school from the class relationship (works even when the staff user had no
  school), plus staff/student **user** accounts from their linked record.
- Result on the seeded DB: all 11 staff and all 72 teacher-class-subjects now carry a
  `school_id`; 21 fee payments (Rs 20,750); 20 homework submissions; only the two
  intentionally multi-school parent accounts remain school-less.

**To apply on a fresh database:** `php artisan migrate:fresh --seed`
**On the current database:** the three seeders were already run —
`php artisan db:seed --class=RolePermissionSeeder`,
`--class=RoleScenarioSeeder`, `--class=DemoNormalizationSeeder`.

### 6. Forgot / reset password — MEDIUM ✅
- `app/Http/Controllers/Auth/PasswordResetController` + guest routes
  `password.request` / `password.email` / `password.reset` / `password.update`.
- Views `auth/forgot-password.blade.php` and `auth/reset-password.blade.php` in the
  existing login styling.
- The login page link now points to `/forgot-password`.
- Verified end-to-end: request → email link → reset form → new password works → redirect
  to login. Email enumeration is prevented (always reports success).

**Production note:** `.env` currently has `MAIL_MAILER=log`, so the reset link is written
to `storage/logs/laravel.log`. Configure SMTP (`MAIL_MAILER=smtp` + host/credentials) for
real delivery.

### 7. Parent portal reachable by non-parents — MEDIUM ✅
`GET /my-children` moved inside the `role:parent` middleware group in `routes/web.php`.
Verified: parent 200, teacher/student/other 403.

### 9. Attendance status validation — LOW ✅
`AttendanceController@store` now validates `status.*` against
`in:present,absent,late,half_day,holiday`.

### 10. "Change Password" menu link — LOW ✅
Added `id="change-password"` to the password card on the profile page; the header menu
link now jumps to it (`/profile#change-password`).

### 13. Hostel assign null-guard — LOW ✅
`resources/views/hostel/assign.blade.php` now null-safely resolves each hostel option.

### Test suite could wipe the development database — CRITICAL (found while verifying) ✅
`phpunit.xml` set `DB_DATABASE=smart_school_test` without `force="true"`. Because
`php artisan test` loads `.env` first (`DB_DATABASE=smart_school`), the phpunit value was
ignored and `RefreshDatabase` ran `migrate:fresh` against the **development** database.
Added `force="true"` to the test env vars so the suite always targets
`smart_school_test`. (If your MySQL root password isn't `Root@123`, run
`DB_PASSWORD= php artisan test`.)

---

## Still open

### 8. No teacher scoping on attendance / marks entry — MEDIUM (needs a product decision)
Any teacher can still pick any class/section and write attendance or marks. Fine for a
small school where staff cover each other; a problem at scale. If you want it enforced,
restrict the class/section pickers (and the `store` methods) to the teacher's
`TeacherClassSubject` rows — say the word and I'll wire it up.

### Mobile app: teacher login
Currently blocked at the API. Decide whether teachers get a mobile role; if yes it's a
one-line change plus a check that the portal endpoints behave for a teacher context.

### 11. Production config
Ensure the production `.env` sets `APP_DEBUG=false` and a real mailer.

---

## New feature — Parent-Teacher Meetings (PTM)

Built this session. Teacher publishes availability → parent books a slot for a child →
teacher confirms or declines → both sides notified in-app.

**Data:** `ptm_slots` (teacher, date, start/end, location, is_open), `ptm_bookings`
(slot, parent, student, status pending/confirmed/rejected/cancelled, notes).

**Teacher / admin** — `/ptm` (sidebar: Academics → *Parent Meetings*):
- "Publish availability" — two modes:
  - **Split a time window** — a date + window + slot length → auto-splits into
    back-to-back slots.
  - **Add one custom slot** — type an exact start and end time for a single slot.
- "Requests awaiting confirmation" — **Confirm** (one click) or **Decline** (with an
  optional message to the parent).
- Confirming a slot **automatically declines every other pending request for that same
  slot**, and notifies those parents ("This slot has been taken — please pick another").
- Admin sees and manages the whole school's slots; teachers see only their own.

**Parent** — `/my/meetings` (sidebar: My Portal → *Meetings*) and mobile *Meetings* screen:
- Sees each child's teachers with their open upcoming slots; taps a slot, picks the
  child, adds a note, sends the request.
- "My requests" shows live status: **Awaiting confirmation** (yellow), **Confirmed**
  (green, "come to school at this time"), **Declined** (red, with the teacher's message
  and a prompt to pick another slot). Can cancel a pending or confirmed booking.

**Notifications:** every decision writes a `UserNotification`, so it appears in the web
bell dropdown *and* the mobile Announcements screen.

**Mobile:** `GET /api/v1/ptm`, `POST /api/v1/ptm/slots/{slot}/book`,
`POST /api/v1/ptm/bookings/{booking}/cancel`; new Flutter `PtmScreen`
(`flutter analyze`: no issues).

**Guards:** a parent can only book a slot belonging to one of their own child's
teachers; a student / other role cannot reach the teacher pages (403).

**Tests:** `tests/Feature/PtmTest.php` — 9 tests covering both publish modes, the
book→confirm flow, the auto-decline-on-confirm conflict rule, decline-with-message, and
the access guards.

**Also fixed while wiring PTM:** parent/student **messaging recipient list was empty** on
this database — `messageableRecipients()` reads the `assign_subjects` table, which the
demo seeders never populated (they filled `teacher_class_subjects` instead).
`DemoNormalizationSeeder` now mirrors the assignments across, and
`schools:backfill-tenant-ids` derives `assign_subjects.school_id`. Two-way messaging
verified working on web and API afterwards.

---

## Regression check

- Full authenticated page sweep for every role: **no 500s, no error pages**.
- Mobile API (parent + student) and public guest endpoints: **200 across the board**.
- Super-admin **enter → banner → exit** flow verified end to end.
- Forgot-password flow verified end to end (request → link → reset → login).
- Parent-Teacher Meeting flow verified end to end on web and API (book, confirm, decline,
  auto-decline conflict, cancel, notifications).
- **`php artisan test`: 102 passed (300 assertions), 0 failures.**
