Detailed Project Documentation · Views, Fields & Full Issue Log
Adhrit LMS — Learning Management Suite
Every admin and site view described as a User Story with its full field list, mapped to file-level tasks and progress — plus a complete Issues & Fixes Log of every requirement and bug ever opened or resolved across the suite (component, three modules, plugin, REST API).
Contents
- Delivery Summary
- Delivery by Component
- REST API & Postman Testing
- Platform, Architecture & Packaging
- Admin Dashboard & Navigation
- Course Content Management
- Course Editor
- Assessments & Quiz Gating
- People & Events
- Commerce & Payments
- Certificates & Verification
- Settings & Configuration
- Backup, Restore & Bulk Import
- Frontend Learner Experience
- REST API & Mobile
- Companion Site Modules
- Security Scan Report
- Teacher Portal & Media Governance
- Issues & Fixes Log
com_adhritlms/ (admin / site / api apps) or the module/plugin root.Delivery Summary
| Epic | Scope | Status | Completion |
|---|---|---|---|
| E1 · Platform, Architecture & Packaging | MVC bootstrap, SEF router, 18-table schema + idempotent migration, one-step package | DONE | |
| E2 · Admin Dashboard & Navigation | Cpanel dashboard (KPIs, charts), grouped collapsible sidebar, mobile drawer | DONE | |
| E3 · Course Content Management | Course → Chapter → Lesson + Categories: list/edit views, cascades, CSV import | DONE | |
| E4 · Course Editor | Single-screen Course Editor: in-place forms, clone/reuse, drag reorder, content map | DONE | |
| E5 · Assessments & Quiz Gating | Quiz authoring + results + server-side Lesson→Chapter→Course gating engine | DONE | |
| E6 · People & Events | Teacher & speaker profiles, events with mapped venues + online links | DONE | |
| E7 · Commerce & Payments | Orders, invoices, coupons, reviews + working PayPal (IPN) & UPI (deep-link/QR) checkout | ACTIVE | |
| E8 · Certificates & Verification | Customizable certificates (4 templates, borders/frames) + public verification | DONE | |
| E9 · Settings & Configuration | Consolidated settings (General/Payment/Certificates/Appearance/Editor/Invoices/ACL) | DONE | |
| E10 · Backup, Restore & Bulk Import | Full JSON backup/restore (2 modes) + per-course JSON/XML + CSV importers | DONE | |
| E11 · Frontend Learner Experience | Catalog, course page, gated lesson player, checkout, tabbed My Learning hub | IN PROGRESS | |
| E12 · REST API & Mobile | Catalog JSON:API (read-only) + per-user Learner API (login/token, me/*, learn, orders) | ACTIVE | |
| E13 · Companion Site Modules | Three template-agnostic modules: Catalog, Recommended, Carousel | IN PROGRESS | |
| E14 · Security Scan Report | Full security audit snapshot: 0 Critical/High/Medium, 5 Low fixed (4.2.17); all areas verified secure | DONE | |
| E15 · Teacher Portal & Media Governance | Frontend Teacher Portal + Teacher REST API: own-items authoring, governed uploads, JSON import/export | DONE | |
| Whole suite | Component + 3 modules + Web Services plugin + REST API | ACTIVE |
Delivery by Component
com_adhritlms (component)
The full LMS: platform/packaging, admin dashboard & navigation, course content, course editor, assessments & gating, people/events, commerce, certificates, settings, backup and the whole frontend learner experience.
| Story | View / Area | Status |
|---|---|---|
| US 1.1 | Component bootstrap & routing | DONE |
| US 1.2 | Install, schema & migration | DONE |
| US 1.3 | One-step package | DONE |
| US 2.1 | Dashboard — Cpanel view | DONE |
| US 2.2 | Component sidebar & menu visibility | DONE |
| US 2.3 | Mobile navigation drawer | DONE |
| US 3.1 | Courses — list view | DONE |
| US 3.2 | Course — edit view | DONE |
| US 3.3 | Course Categories — list & edit | DONE |
| US 3.4 | Chapters — list & edit | DONE |
| US 3.5 | Lessons — list & edit | DONE |
| US 4.1 | Course Editor — builder | DONE |
| US 4.2 | Content reuse, reorder & map | DONE |
| US 5.1 | Quiz Questions — list & edit | DONE |
| US 5.2 | Quiz Results — list & edit | DONE |
| US 5.3 | Quiz taking & server-side gating | DONE |
| US 6.1 | Teachers — list & edit | DONE |
| US 6.2 | Speakers — list & edit | DONE |
| US 6.3 | Events & Event Categories | DONE |
| US 7.1 | Orders — list & edit | DONE |
| US 7.2 | Invoices — list & printable invoice | DONE |
| US 7.3 | Coupons & Reviews | DONE |
| US 7.4 | Checkout & working payments | ACTIVE |
| US 8.1 | Certificate design (Settings) & admin records | DONE |
| US 8.2 | Certificate render, download, share & verify | DONE |
| US 9.1 | General (with Security / Profile / Maps sections) | DONE |
| US 9.2 | Payment settings | DONE |
| US 9.3 | Certificates settings | DONE |
| US 9.4 | Appearance settings | DONE |
| US 9.5 | Course Editor settings | DONE |
| US 9.6 | Invoice settings | DONE |
| US 9.7 | Permissions | DONE |
| US 10.1 | Backup & Restore — view | DONE |
| US 10.2 | Per-course export/import & CSV imports | DONE |
| US 11.1 | Courses catalog & category views | DONE |
| US 11.2 | Course page (enroll) | DONE |
| US 11.3 | Lesson player | DONE |
| US 11.4 | Cart / Checkout / Purchases | DONE |
| US 11.5 | My Learning — profile hub | DONE |
| US 11.6 | Invoice — printable view | IN PROGRESS |
| US 14.1 | SQL Injection | DONE |
| US 14.2 | Cross-Site Scripting (XSS) & output encoding | DONE |
| US 14.3 | Cross-Site Request Forgery (CSRF) | DONE |
| US 14.4 | Broken Access Control / IDOR | DONE |
| US 14.5 | File Upload & Path Traversal | DONE |
| US 14.6 | Authentication & Session Tokens | DONE |
| US 14.7 | SSRF & Open Redirect | DONE |
| US 14.8 | Unsafe PHP / Injection Sinks | DONE |
| US 14.9 | Low-severity findings fixed in 4.2.17 | DONE |
| US 15.1 | Teacher user group & access | DONE |
| US 15.2 | Frontend course editor | DONE |
| US 15.3 | Media governance | DONE |
| US 15.4 | Own-items only & deletion policy | DONE |
| US 15.5 | Teacher REST API (mobile) | DONE |
| US 15.6 | JSON course import / export | DONE |
| US 15.7 | Drag-and-drop content editor | DONE |
| US 15.8 | Teacher profile media, email & website | DONE |
| US 15.9 | Teacher API switch | DONE |
mod_adhritlmscatalog (Catalog module)
One configurable site module that lists any LMS content type (courses, categories, teachers, events, event categories) or a course-search box, with a 1×1–4×4 grid and a CTA button.
| Story | View / Area | Status |
|---|---|---|
| US 13.1 | Catalog module | DONE |
mod_adhritlmsrecommended (Recommended module)
Personalised course recommendations — learner preferences → enrolled categories; guests → admin picks; hide-enrolled and latest top-up. Never cached.
| Story | View / Area | Status |
|---|---|---|
| US 13.2 | Recommended Courses module | DONE |
mod_adhritlmscarousel (Carousel module)
A full carousel/slider: Banner/Slim/Small modes, 20 presets, 20 animations, 20 Bootstrap-Icon arrows, 11 clickable bullet types with orientation, per-slide colour/icon/thumbnail, spacing controls, typography and CTA. Robust image handling — a missing/renamed image degrades to the placeholder and never blanks the carousel.
| Story | View / Area | Status |
|---|---|---|
| US 13.3 | Carousel module — Content | DONE |
| US 13.4 | Carousel module — Layout & Style | DONE |
| US 13.5 | Carousel module — Carousel controls | IN PROGRESS |
| US 13.6 | Carousel module — Typography, Button & Appearance | DONE |
plg_webservices_adhritlms (Web Services plugin)
Registers all Adhrit LMS REST routes with the Joomla API application — the read-only catalog JSON:API and the authenticated per-user Learner API (login/token, me/*, learning actions, orders).
| Story | View / Area | Status |
|---|---|---|
| US 12.1 | Catalog JSON:API | DONE |
| US 12.2 | Learner API (login + me/*) | ACTIVE |
| US 12.3 | Teacher API (mobile course authoring) | DONE |
Web Services (REST API) — Endpoints & Postman Testing
https://yoursite.com/api/index.php: a read-only catalog JSON:API (Joomla API token), an authenticated per-user Learner API (rotating token from /login), and a Teacher API for course authoring (same login token + Teacher-group membership). All paths below are relative to https://yoursite.com/api/index.php.X-Adhrit-Token header only; do NOT place the adhrit token in Authorization: Bearer (Joomla's API layer authenticates that header itself and returns 403). Catalog endpoints use X-Joomla-Token. Toggle each surface, set a token TTL and Require-HTTPS under Components → Adhrit LMS → Options → API Settings, and export a ready-made Postman collection from there. HTTPS enforcement is a backstop — if the web server redirects HTTP→HTTPS the app only sees the upgraded request, so enforce HTTPS at the server (redirect + HSTS). Full setup, Global-Config checklist and secure mobile token storage are in the separate Adhrit LMS — API Integration Guide.Catalog API — read-only (auth: X-Joomla-Token)
| Method | Path | Auth header | Purpose |
|---|---|---|---|
| GET | /v1/adhritlms/courses |
X-Joomla-Token |
List courses (JSON:API); ?category={id} to scope. Each course carries teacher_ids, chapter_count, lesson_count, review_count, rating_avg, has_final_quiz, currency, price_display |
| GET | /v1/adhritlms/courses/{id} |
X-Joomla-Token |
A single course (same enriched fields) |
| GET | /v1/adhritlms/courses/{id}/curriculum |
X-Joomla-Token |
One-call nested curriculum: course + chapters → ordered lessons (is_preview, duration, type, has_quiz). Token-authenticated; no course enrolment required |
| GET | /v1/adhritlms/coursescategories |
X-Joomla-Token |
List course categories (+ /{id}); each carries course_count |
| GET | /v1/adhritlms/chapters |
X-Joomla-Token |
List chapters (+ /{id}); ?course={id} to scope; carry adhritlms_course_id + lesson_count |
| GET | /v1/adhritlms/lessons |
X-Joomla-Token |
List lessons (+ /{id}); ?course={id}&chapter={id} to scope; carry both FKs + has_quiz |
| GET | /v1/adhritlms/teachers |
X-Joomla-Token |
List teachers (+ /{id}); carry course_ids |
| GET | /v1/adhritlms/speakers |
X-Joomla-Token |
List speakers (+ /{id}); carry event_ids |
| GET | /v1/adhritlms/events |
X-Joomla-Token |
List events (+ /{id}); carry speaker_ids |
Learner API — per-user (auth: X-Adhrit-Token from /login)
| Method | Path | Auth | Body / params | Purpose |
|---|---|---|---|---|
| POST | /v1/adhritlms/login |
none | {"username":"user","password":"pass"} |
Sign in; returns a rotating bearer token |
| POST | /v1/adhritlms/logout |
X-Adhrit-Token |
— | Invalidate the current token |
| GET | /v1/adhritlms/me |
X-Adhrit-Token |
— | Profile, avatar, preferences |
| POST | /v1/adhritlms/me/preferences |
X-Adhrit-Token |
{"categories":[1,4,7]} |
Save favourite categories |
| GET | /v1/adhritlms/me/orders |
X-Adhrit-Token |
— | All of the learner's orders (number, course, amount, status, date) |
| POST | /v1/adhritlms/me/avatar |
X-Adhrit-Token |
multipart: avatar=@file |
Upload avatar (JPG/PNG/WebP ≤ 2 MB) |
| GET | /v1/adhritlms/me/courses |
X-Adhrit-Token |
— | Enrolled courses + progress % |
| GET | /v1/adhritlms/me/courses/{id}/curriculum |
X-Adhrit-Token |
— | Chapters+lessons with completed/quiz/locked flags |
| GET | /v1/adhritlms/lessons/{id}/content |
X-Adhrit-Token |
— | Full lesson (enrolment / preview gated) |
| POST | /v1/adhritlms/courses/{id}/enroll |
X-Adhrit-Token |
— | Free enrols instantly; paid returns 402 |
| POST | /v1/adhritlms/lessons/{id}/complete |
X-Adhrit-Token |
— | Record progress; may issue certificate |
| GET | /v1/adhritlms/quiz?course_id=N |
X-Adhrit-Token |
(+ &lesson_id=N | &chapter_id=N) |
Questions WITHOUT correct answers; each carries lesson_id/chapter_id/scope. Add &lesson_id=Y for one lesson's quiz |
| POST | /v1/adhritlms/quiz/submit |
X-Adhrit-Token |
{"course_id":N,"lesson_id":N,"answers":{"12":"B","15":["A","C"]}} |
Server-side scoring; stores result |
| POST | /v1/adhritlms/orders |
X-Adhrit-Token |
{"course_id":N,"payment_method":"paypal|upi|bank","coupon_code":""} |
Create paid order → payment instructions |
| GET | /v1/adhritlms/me/orders/{id} |
X-Adhrit-Token |
— | Poll an order's status |
| GET | /v1/adhritlms/me/invoices |
X-Adhrit-Token |
— | The learner's invoices |
| GET | /v1/adhritlms/me/certificates |
X-Adhrit-Token |
— | The learner's certificates |
| GET | /v1/adhritlms/me/wishlist |
X-Adhrit-Token |
— | Wishlist courses |
| POST | /v1/adhritlms/me/wishlist/{id} |
X-Adhrit-Token |
— | Toggle a course in the wishlist |
| PUT | /v1/adhritlms/me/wishlist/{id} |
X-Adhrit-Token |
— | Add a course to the wishlist (idempotent) |
| DELETE | /v1/adhritlms/me/wishlist/{id} |
X-Adhrit-Token |
— | Remove a course from the wishlist (idempotent) |
Teacher API — course authoring (auth: X-Adhrit-Token; user must be in the Teacher group)
created_by); teachers can't touch another teacher's content and can't create categories. Master switch: Options → API Settings → Enable Teacher API (off → all rows below return 403 and the Teacher folder is dropped from the Postman export). Send Accept: */* — NOT application/json (Joomla's API replies 406 to it on these routes).| Method | Path | Auth | Body / params | Purpose |
|---|---|---|---|---|
| GET | /v1/adhritlms/teacher/courses |
X-Adhrit-Token |
— | List the teacher's own courses |
| POST | /v1/adhritlms/teacher/courses |
X-Adhrit-Token |
{"title":"...","short_description":"...","price":0,"level":"beginner"} |
Create a course (only admin-enabled fields saved) → course_id |
| GET | /v1/adhritlms/teacher/courses/{id} |
X-Adhrit-Token |
— | One owned course + its chapters, lessons, quiz |
| PUT | /v1/adhritlms/teacher/courses/{id} |
X-Adhrit-Token |
{"title":"...","price":19.99} |
Update an owned course |
| DELETE | /v1/adhritlms/teacher/courses/{id} |
X-Adhrit-Token |
— | Delete an owned course (only if admin allows teacher deletion) |
| POST | /v1/adhritlms/teacher/chapters |
X-Adhrit-Token |
{"adhritlms_course_id":N,"title":"...","ordering":0} |
Add/update a chapter in an owned course |
| POST | /v1/adhritlms/teacher/lessons |
X-Adhrit-Token |
{"adhritlms_course_id":N,"adhritlms_chapter_id":M,"title":"...","description":"...","lesson_type":"video","video_url":"..."} |
Add/update a lesson |
| POST | /v1/adhritlms/teacher/questions |
X-Adhrit-Token |
{"adhritlms_course_id":N,"question":"...","question_type":"single","marks":1,"choices":[{"text":"A"}],"correct_row":0} |
Add/update a quiz question (chapter/lesson bound) |
| DELETE | /v1/adhritlms/teacher/items/{id}?type=chapter|lesson|question |
X-Adhrit-Token |
— | Delete an owned chapter/lesson/question (admin-permitting) |
| POST | /v1/adhritlms/teacher/courses/{id}/image |
X-Adhrit-Token |
multipart: image=@file |
Upload course image (PNG/JPG ≤ 500 KB) into the teacher's folder |
| POST | /v1/adhritlms/teacher/courses/{id}/pdf |
X-Adhrit-Token |
multipart: pdf=@file |
Attach a course PDF (≤ 2 MB) |
| GET | /v1/adhritlms/teacher/profile |
X-Adhrit-Token |
— | Get the teacher's own profile |
| PUT | /v1/adhritlms/teacher/profile |
X-Adhrit-Token |
{"title":"...","designation":"...","email":"...","website":"https://..."} |
Update own profile (social links validated) |
| GET | /v1/adhritlms/teacher/preferences |
X-Adhrit-Token |
— | Get preferred course categories |
| PUT | /v1/adhritlms/teacher/preferences |
X-Adhrit-Token |
{"categories":[1,4]} |
Save preferred categories |
| GET | /v1/adhritlms/teacher/courses/{id}/export |
X-Adhrit-Token |
— | Export an owned course as JSON (media links omitted unless admin enables) |
| POST | /v1/adhritlms/teacher/import |
X-Adhrit-Token |
{"format":"adhritlms-course","course":{...},"chapters":[],"lessons":[],"quiz":[]} |
Import a course from JSON (creates a course the teacher owns) |
Testing in Postman — step by step
In Joomla admin, go to System → Manage → Plugins and enable: Web Services - Adhrit LMS (registers the routes), Web Services - Joomla, and API Authentication - Web Services Joomla Token. The API app answers at https://yoursite.com/api/index.php. If that URL 404s, confirm the /api/ app is reachable (URL rewriting / .htaccess) — the catalog endpoints render at /api/index.php/v1/adhritlms/....
The read-only catalog JSON:API uses a standard Joomla token. Open Users → Manage → (your user) → Joomla API Token tab, click Generate, and copy the token. This is only needed for the catalog routes; the Learner API uses its own token from /login.
Create an environment with three variables: base_url = https://yoursite.com/api/index.php, joomla_token (from step 2), and adhrit_token (filled automatically in step 5). Using variables lets every request read {{base_url}} and the right token.
New request → GET {{base_url}}/v1/adhritlms/courses. Headers: Accept: application/vnd.api+json and X-Joomla-Token: {{joomla_token}}. Send — you should get a JSON:API list of courses. Append /1 to fetch a single course.
New request → POST {{base_url}}/v1/adhritlms/login. Headers: Content-Type: application/json, Accept: application/json. Body → raw JSON: {"username":"yourlogin","password":"yourpass"}. The response is {"success":true,"token":"...","user":{...}}. In the request's Scripts → Post-response tab add: pm.environment.set('adhrit_token', pm.response.json().token); so the token is stored automatically. (Each login rotates the token — the previous one stops working.)
New request → GET {{base_url}}/v1/adhritlms/me. Set Authorization = No Auth and add the header X-Adhrit-Token: {{adhrit_token}}. Send — you get the signed-in learner's profile. Every Learner-API row in the table above works the same way: No Auth + that one header. Important: do NOT put the adhrit token in Authorization: Bearer — Joomla's API layer authenticates the Authorization header itself and will reject a non-Joomla token there with a 403 before the endpoint runs. The adhrit token belongs only in X-Adhrit-Token.
Try, in order: GET /me/courses → GET /me/courses/{id}/curriculum → GET /lessons/{id}/content → POST /lessons/{id}/complete. For assessments: GET /quiz?course_id=N then POST /quiz/submit with a body like {"course_id":N,"answers":{"12":"B","15":["A","C"]}}. For buying: POST /courses/{id}/enroll (free) or POST /orders (paid → returns PayPal URL / UPI intent / bank details), then poll GET /me/orders/{id}.
The Teacher API uses the very same adhrit_token from step 5 — no separate login — but the signed-in user must be in the Joomla Teacher group (else 403). Try: GET /teacher/courses → POST /teacher/courses (returns course_id) → POST /teacher/chapters and /teacher/lessons (send adhritlms_course_id) → POST /teacher/questions with choices + correct_row → upload a cover with POST /teacher/courses/{id}/image (Body → form-data, key image, type File). Set header Accept: */*. The generated Postman collection already contains a Teacher (adhrit token) folder with all of these wired up.
401 = missing/expired token (log in again — tokens rotate). 402 on enroll = the course is paid, use POST /orders instead. 403 on a teacher/* route = the user isn't in the Teacher group, or the Teacher API is switched off. 406 (Could not match accept header) = you sent Accept: application/json on a custom route — use Accept: */*. Quiz answers are never returned by GET /quiz; scoring happens server-side on submit. Use application/vnd.api+json on catalog calls.
Platform, Architecture & Packaging
Adhrit LMS is a native Joomla 5/6 component (com_adhritlms) built on the modern MVC stack (PSR-4 namespace Adhrit\Component\Adhritlms, service-provider bootstrapping, namespaced Models/Tables/Views, a SEF RouterView, and separate admin / site / api applications). Content lives in 18 #__adhritlms_* tables. The whole suite ships as one upgrade package that also carries three site modules and the Web Services plugin. An idempotent install script creates tables and back-fills any missing columns on every update, so upgrades never require manual SQL.
| Task | Technical work | Progress |
|---|---|---|
| Manifest, namespace, service providers | admin/adhritlms.xml, admin/services/provider.php, site/services/provider.php | DONE |
| SEF router with exact view matching | site/src/Service/Router.php; AdhritlmsHelper::getItemId() exact match |
DONE |
| API application entry | admin/adhritlms.xml <api> block → api/src | DONE |
| Task | Technical work | Progress |
|---|---|---|
| Install/uninstall SQL (18 tables) | admin/sql/install/mysql/install.sql, sql/uninstall/mysql/uninstall.sql | DONE |
| Idempotent migration | script.php Com_AdhritlmsInstallerScript::ensureTables()/ensureColumns() |
DONE |
| Table classes + aliases | admin/src/Table/*.php — setColumnAlias('published','enabled'); JSON encode of subform values in check() |
DONE |
| Task | Technical work | Progress |
|---|---|---|
| Package manifest | pkg_adhritlms.xml — component + catalog + recommended + carousel modules + webservices plugin | DONE |
| Build pipeline (lint/validate/scan) | php -l, simplexml validate, INI parse, forbidden-string scan on every build | DONE |
Admin Dashboard & Navigation
Every admin management page is wrapped in a grouped, collapsible dark sidebar rendered by AdhritlmsHelper::renderAdminSidebar(). The Dashboard (Cpanel view) is a modular, responsive dark board of KPI cards, a sales chart and recent-activity tables. Menu groups are opt-in and the sidebar collapses to a mobile drawer under 900px.
- Summary cards: Courses, Orders, Users, Revenue
- Sales-Trends chart — filters: date range, course, category
- KPI strip: orders & revenue this month, avg order value, certificates issued, lessons completed, reviews
- Order-status donut (pending / completed / cancelled)
- Shortcut buttons: Settings, Course Editor, Courses, Lessons, Quiz, Orders, Reviews, Events, Speakers, Teachers
- Shared KPI filter: Year / Month / Course / Category (defaults to current month)
| Task | Technical work | Progress |
|---|---|---|
| Dashboard view + data helpers | admin/src/View/Cpanel/HtmlView.php, admin/tmpl/cpanel/default.php; dashboardKpis(), salesTrend(), orderStatusBreakdown(), recentOrdersDetailed(), recentCoursesDetailed(), kpiYearOptions() |
DONE |
| Charts via Chart.js (CDN) | donut + trend line; responsive Bootstrap grid blocks | DONE |
- Quick buttons: Dashboard, Course Editor (unique icons) + shrink toggle
- Course Management: Courses, Categories, Chapters, Lessons, Quiz Questions, Quiz Results, Certificates
- Talent Management: Teachers
- Events: Events, Event Categories, Speakers
- Order Management: Orders, Invoices, Coupons, Reviews
- Pinned: Backup & Restore, Settings
- Per-group visibility from Settings → General (nav_show_*)
| Task | Technical work | Progress |
|---|---|---|
| Sidebar renderer + visibility gating | Helper/AdhritlmsHelper.php renderAdminSidebar() / renderAdminSidebarEnd() — raw single-escaped URLs, nav_show_* checks |
DONE |
| Collapsible groups + shrink rail | scoped dark CSS + adlNavToggle/adlNavMiniToggle, state in localStorage |
DONE |
| Stale list-filter guard | validateFilterId() resets remembered course/chapter/lesson/category filters pointing at deleted records |
DONE |
| Task | Technical work | Progress |
|---|---|---|
| Off-canvas drawer | Helper/AdhritlmsHelper.php adminSidebarAssets() — adlNavMobToggle(), backdrop, media query |
DONE |
Course Content Management
The heart of the LMS: the Course → Chapter → Lesson hierarchy plus categories. Each entity has an admin list view (bulk publish/unpublish/delete, per-row status toggle, search & relational filters) and an admin edit view (its form). Deletions cascade to keep referential integrity, and categories, chapters and lessons support packaged-template CSV import.
- Checkbox / bulk select
- Status toggle (enabled)
- Title (link to edit) + Free badge
- Category
- Price (configured currency)
- Level
- Featured (star)
- ID
- Filters: search, category, published state
- Bulk: publish / unpublish / delete
- Export selected → JSON or XML (with chapters/lessons/quizzes)
- Import → restore as new courses
| Task | Technical work | Progress |
|---|---|---|
| List view/model/controller | admin/src/{View,Model,Controller}/Courses*, admin/tmpl/courses/default.php | DONE |
| Per-course export / import | admin/src/Controller/CoursesController.php export() / import() (JSON+XML, id remap, slug dedupe, category match/create) |
DONE |
| Field | Type | Purpose |
|---|---|---|
title |
text | Course title (required) |
slug |
text | URL alias (auto from title if blank) |
adhritlms_coursescategory_id |
sql | Course category (from #__adhritlms_coursescategories) |
short_description |
textarea | Card / listing teaser |
description |
editor | Full rich-text description |
image |
media | Feature image (Media Manager) |
price |
number | Regular price (0 with is_free = free) |
sale_price |
number | Optional sale price (used when > 0) |
is_free |
radio | Free course toggle (still requires login+enrol) |
teachers |
sql | Assigned teacher(s), many-to-many |
show_teacher |
radio | Show instructor block on the course page |
featured_course |
radio | Feature flag (used by modules/filters) |
course_level |
list | beginner / intermediate / advanced |
course_language |
text | Language label |
course_duration |
text | Duration label |
what_will_learn |
textarea | Learning outcomes list |
requirements |
textarea | Prerequisites list |
certificate_enabled |
radio | Issue a certificate on completion |
quiz_pass_percentage |
number | Pass mark % for this course's quizzes |
quiz_revisit |
radio | Allow retaking a passed quiz |
enabled |
radio | Published status (aliased to 'published') |
ordering |
number | Manual sort order |
meta_title |
text | SEO meta title |
meta_description |
textarea | SEO meta description |
meta_keywords |
textarea | SEO meta keywords |
| Task | Technical work | Progress |
|---|---|---|
| Course form + model | admin/forms/course.xml, admin/tmpl/course/default.php, admin/src/Model/CourseModel.php (teacher pivot sync, JSON schedule/info decode) | DONE |
| Live invoice tax breakup | admin/tmpl/course/default.php — reads inv_taxes, JS subtotal/tax/total panel |
DONE |
| Cascade delete + publish | CourseModel::delete()/publish() cascade chapters, lessons, quiz questions, teacher links |
DONE |
| Field | Type | Purpose |
|---|---|---|
title |
text | Category name |
slug |
text | URL alias |
description |
editor | Rich description |
image |
media | Category image |
enabled |
radio | Published status |
ordering |
number | Sort order |
| Task | Technical work | Progress |
|---|---|---|
| List + edit screens | admin/src/{View,Model,Controller}/Coursescategor(y/ies)*, admin/forms/coursescategory.xml | DONE |
| CSV import | admin/src/Controller/CoursescategoriesController.php importcsv(); media/com_adhritlms/import/categories_template.csv |
DONE |
| Field | Type | Purpose |
|---|---|---|
title |
text | Chapter title |
slug |
text | URL alias |
adhritlms_course_id |
sql | Owning course |
description |
editor | Chapter description (shown on frontend) |
resources |
subform | Repeatable downloads — each row: title (text), type (list: pdf/zip/link), file (path), url (external) |
enabled |
radio | Published status |
ordering |
number | Sort order within the course |
| Task | Technical work | Progress |
|---|---|---|
| Chapter screens + resources | admin/forms/chapter.xml, admin/src/Model/ChapterModel.php (resources JSON decode; delete detach) | DONE |
| Gated resource download | site/src/Controller/ResourceController.php — login+enrolment, realpath-locked, PDF/ZIP only | DONE |
| Field | Type | Purpose |
|---|---|---|
title |
text | Lesson title |
slug |
text | URL alias |
adhritlms_course_id |
sql | Owning course |
adhritlms_chapter_id |
sql | Chapter (or General = 0) |
lesson_type |
list | video / text / … |
video_url |
url | Video source URL |
video_type |
list | youtube / vimeo / mp4 |
video_duration |
text | Duration label |
description |
editor | Lesson body (rich text) |
attachments |
subform | Repeatable downloads — each row: title (text), file (media picker), url (external) |
is_preview |
radio | Free preview (viewable without enrolment) |
enabled |
radio | Published status |
ordering |
number | Sort order |
| Task | Technical work | Progress |
|---|---|---|
| Lesson screens + attachments | admin/forms/lesson.xml, admin/src/Model/LessonModel.php (JSON+legacy decode; delete detach) | DONE |
| Lessons CSV import | admin/src/Controller/LessonsController.php importcsv(); media/com_adhritlms/import/lessons_template.csv |
DONE |
Course Editor
A single-screen builder (Courseeditor view) that assembles a whole course — chapters, lessons and quizzes — from one tree without hopping between list views. Add/Edit opens the real admin edit form in-place (all fields, full validation) via an iframe modal and returns to the editor on save. All AJAX actions are CSRF-protected and permission-gated, and everything writes to the same tables the list views use.
- Course selector + New Course + Full settings + Preview
- Tree: chapters → lessons, quiz badges, preview/duration markers
- Toolbar: Add chapter, Add existing chapter, Add lesson, Add existing lesson
- Per-item: edit / delete / add quiz / reuse quiz
- Floating bar (on scroll) + back-to-top
| Task | Technical work | Progress |
|---|---|---|
| Editor view + AJAX endpoints | admin/src/View/Courseeditor/HtmlView.php, admin/tmpl/courseeditor/default.php, admin/src/Controller/CourseeditorController.php (getTree/saveCourse/saveLesson/deleteChapter/deleteLesson/getQuiz…) | DONE |
| In-place real edit form (iframe modal) | opens course/chapter/lesson/quiz edit forms; returns on save | DONE |
| Editor theming | config.xml ceditor fieldset (ce_text/chapter/lesson/accent/quiz-badge colours + glow) |
DONE |
| Task | Technical work | Progress |
|---|---|---|
| Clone picker + drag reorder | searchable picker clones with children; drag handlers persist ordering | DONE |
| Content-map overview | Courseeditor/HtmlView::buildCourseMap() — counts + expandable tree + Edit shortcut |
DONE |
Assessments & Quiz Gating
Quiz authoring, results management and the server-side gating engine. A question attaches at exactly one scope with priority Lesson → Chapter → Course (final). Gating is enforced on the server so a learner can never skip a required quiz by clicking ahead or pasting a link, and certificates are only issued when the whole course is genuinely finished.
| Field | Type | Purpose |
|---|---|---|
title |
text | Short question title (shown in lists) |
question |
editor | Rich question body (images/video allowed) |
slug |
text | URL alias |
adhritlms_course_id |
sql | Owning course (required) |
adhritlms_lesson_id |
sql | Lesson scope (0 = none) |
adhritlms_chapter_id |
sql | Chapter scope (0 = none) |
question_type |
list | single / multiple / truefalse / text |
list_answers |
textarea | Answer options, one per line |
correct_answer |
textarea | Correct answer(s), comma-separated for multiple |
explanation |
textarea | Shown after submission |
marks |
number | Points for this question |
enabled |
radio | Published status |
ordering |
number | Order within the quiz |
- Status, Question, Course, Chapter, Lesson, Type, Marks, ID
- Filters: search, course → chapter → lesson, published
| Task | Technical work | Progress |
|---|---|---|
| Questions list/edit + filters | admin/src/{View,Model,Controller}/Quizquestion(s)*, admin/forms/quizquestion.xml | DONE |
| Bulk CSV import | QuizquestionsController.php importcsv(); media/com_adhritlms/import/quizquestions_template.csv |
DONE |
| Field | Type | Purpose |
|---|---|---|
adhritlms_course_id |
sql | Course |
adhritlms_chapter_id |
sql | Chapter scope |
adhritlms_lesson_id |
sql | Lesson scope |
user_id |
user | Student |
total_questions |
number | Questions in the attempt |
correct_answers |
number | Number correct |
score_percentage |
number | Score % |
passed |
radio | Pass/fail flag |
attempt_number |
number | Attempt sequence |
time_taken |
number | Seconds taken |
created_on |
calendar | Attempt timestamp |
- Course, User, Quiz Score, Passed (badge), Attempt, Date, ID
- Filter: search by course/student; passed/failed
| Task | Technical work | Progress |
|---|---|---|
| Results list/edit + fixes | admin/forms/quizresult.xml (created), QuizresultsModel.php (search course/student, passed filter), Table/QuizresultTable.php (no title/slug check) | DONE |
- Question card: title, body, options (radio/checkbox/text)
- Marks badge, Submit
- Result: score, pass/fail, correct answers, explanation
- Post-pass: Mark complete & next lesson
| Task | Technical work | Progress |
|---|---|---|
| Quiz view + scoring | site/src/{View,Controller,Model}/Quiz*, QuizController.php submit(), QuizModel.php submitQuiz() |
DONE |
| Server-side gate + revisit | AdhritlmsHelper::passedQuizResult(); lesson-player lock logic; per-course quiz_revisit |
DONE |
People & Events
Instructor and speaker profiles and a full events programme. Teachers link to courses many-to-many; speakers link to events; events carry scheduling, a mapped venue and an online-meeting URL. Each has an admin list view and edit view, and a matching frontend directory + single-profile view.
| Field | Type | Purpose |
|---|---|---|
title |
text | Teacher name |
slug |
text | URL alias |
designation |
text | Role / title |
experience |
text | Years / experience label |
image |
media | Photo (square) |
description |
editor | Biography |
email |
Contact email | |
phone |
tel | Phone |
website |
url | Website |
facebook |
url | |
twitter |
text | Twitter/X |
linkedin |
url | |
youtube |
url | YouTube |
specialist_in |
textarea | Specialisations |
enabled |
radio | Published |
ordering |
number | Sort order |
| Task | Technical work | Progress |
|---|---|---|
| Teacher screens + pivot | admin/src/{View,Model,Controller}/Teacher(s)*, forms/teacher.xml; #__adhritlms_course_teachers |
DONE |
| Frontend directory + profile | site/src/View/{Teachers,Teacher} + tmpl | DONE |
| Field | Type | Purpose |
|---|---|---|
title |
text | Speaker name |
slug |
text | URL alias |
designation |
text | Role |
company |
text | Company |
image |
media | Photo |
description |
editor | Bio |
email |
||
website |
url | Website |
facebook |
url | |
twitter |
text | Twitter/X |
linkedin |
url | |
enabled |
radio | Published |
ordering |
number | Sort order |
| Task | Technical work | Progress |
|---|---|---|
| Speaker screens + pivot | admin/src/{View,Model,Controller}/Speaker(s)*; #__adhritlms_event_speakers |
DONE |
| Frontend directory + profile | site/src/View/{Speakers,Speaker} + tmpl (rebuilt in 2.1.2) | DONE |
| Field | Type | Purpose |
|---|---|---|
title |
text | Event title |
slug |
text | URL alias |
adhritlms_eventcategory_id |
sql | Event category |
event_type |
list | physical / online / hybrid |
event_start_date |
calendar | Start date/time |
event_end_date |
calendar | End date/time |
event_venue |
text | Venue name |
event_address |
textarea | Address |
event_map_lat |
text | Map latitude |
event_map_lng |
text | Map longitude |
online_url |
url | Online-meeting link (absolute external) |
max_attendees |
number | Capacity |
short_description |
textarea | Teaser |
description |
editor | Full description |
speakers |
sql | Assigned speaker(s) |
image |
media | Event image |
featured |
radio | Feature flag |
enabled |
radio | Published |
ordering |
number | Sort order |
| Task | Technical work | Progress |
|---|---|---|
| Event + category screens | admin/src/{View,Model,Controller}/Event(s)*, Eventcategor(y/ies)*, forms/event.xml, eventcategory.xml | DONE |
| Google Maps venue | map key/height/zoom from config.xml maps section | DONE |
| Frontend events + filters | site/src/View/{Events,Event,Eventcategories,Eventcategory}; search + category + type filter bar | DONE |
Commerce & Payments
Orders, invoices, coupons and reviews management plus a working checkout. Each purchase writes an order; a successful/approved order unlocks the course. PayPal is completed by a verified IPN (receiver + amount checked); UPI shows a GPay/Paytm/PhonePe deep-link and a QR; bank transfer and direct/offline are supported. An optional manual-approval mode holds paid orders as Pending until an admin approves them.
| Field | Type | Purpose |
|---|---|---|
order_number |
text | Auto-generated reference |
user_id |
user | Buyer |
adhritlms_course_id |
sql | Purchased course |
order_payment_price |
number | Amount |
order_currency |
text | Currency code |
payment_method |
list | paypal/upi/bank_transfer/direct/free |
transaction_id |
text | Gateway / reference id |
payment_status |
list | pending/completed/cancelled |
billing_name |
text | Billing name |
billing_email |
Billing email | |
billing_address |
textarea | Billing address |
coupon_code |
text | Applied coupon |
discount_amount |
number | Discount applied |
created_on |
calendar | Order date |
enabled |
radio | Published/active |
- Status badge (Pending/Completed/Cancelled)
- Order #, Course, Billing name, Price, Method, Reference, Date, ID
- Per-row: Approve / Cancel / View Invoice
- Bulk: Approve / Cancel / Delete + Invoice Settings shortcut
| Task | Technical work | Progress |
|---|---|---|
| Orders list/edit + actions | admin/src/{View,Model,Controller}/Order(s)*, admin/tmpl/orders/default.php, forms/order.xml | DONE |
| Manual order + auto number + approval | OrdersController::approve()/cancel(); grants access when completed; payment_manual_approval |
DONE |
- Invoice # (prefix + order number), Course, Billed to (name/email), Total, Status, View, Date, ID
- Publish / unpublish / delete (hides the customer's invoice)
| Task | Technical work | Progress |
|---|---|---|
| Invoices admin list | admin/src/{View,Model,Controller}/Invoices*, admin/tmpl/invoices/default.php | DONE |
| Printable invoice + settings | site/src/View/Invoice/HtmlView.php, site/tmpl/invoice/default.php (3 templates, multi-tax tax-inclusive breakdown, print/PDF) | DONE |
| Field | Type | Purpose |
|---|---|---|
coupon_code |
text | Discount code |
discount_type |
list | percent / fixed |
discount_value |
number | Amount or % |
max_uses |
number | Usage limit |
valid_from |
calendar | Start date |
valid_to |
calendar | Expiry date |
adhritlms_course_id |
sql | Optional course scope |
enabled |
radio | Active |
— Review — |
(second form) | |
adhritlms_course_id |
sql | Reviewed course |
user_id |
user | Reviewer |
rating |
list | 1–5 stars |
review |
textarea | Review text |
created_on |
calendar | Date |
enabled |
radio | Published |
| Task | Technical work | Progress |
|---|---|---|
| Coupon & review screens | admin/src/{View,Model,Controller}/Coupon(s)*, Review(s)* (course/rating/status/search filters) | DONE |
- Order summary + coupon apply
- Billing: name, email, address
- Payment methods: PayPal, Bank transfer, UPI, Direct
- UPI: Pay-via-app deep link, QR, transaction reference
- Complete Purchase → order + redirect/instructions
| Task | Technical work | Progress |
|---|---|---|
| PayPal redirect + IPN + return | site/src/Controller/PaymentController.php process()/redirectToPayPal()/ipn()/paypalReturn() — postback verify, receiver+amount checks, txn stored |
ACTIVE |
| UPI deep-link + QR | site/tmpl/cart/default.php — upi://pay button + qrcode.js; INR amount embedded |
ACTIVE |
Certificates & Verification
Fully customizable completion certificates issued only when a course is genuinely finished (certificates enabled, all lessons complete, all lesson & chapter quizzes passed, final quiz passed). Learners download a PNG/PDF generated entirely in the browser and share to LinkedIn/Facebook/X; a public verification page confirms a certificate by number and is captcha-protected.
| Field | Type | Purpose |
|---|---|---|
cert_template |
list | Classic / Modern / Elegant / Minimal |
cert_accent_color |
color | Accent colour |
cert_border_style |
list | solid/double/dashed/dotted/none |
cert_border_width |
number | Border thickness (px) |
cert_border_color |
color | Border colour |
cert_inner_frame |
list | none/line/double/corners/ornate |
cert_frame_color |
color | Inner-frame colour |
cert_logo |
media | Logo |
cert_heading |
text | Heading text |
cert_body |
textarea | Body with {name}{course}{date}{score}{number} |
cert_number_position |
list | footer / corner / hidden |
cert_show_score |
radio | Show quiz score |
cert_signatory_name |
text | Signatory name |
cert_signatory_title |
text | Signatory title |
cert_signature_image |
media | Signature image |
cert_show_verify_link |
radio | Show verification link |
cert_preview |
certpreview | Preview sample certificate button |
| Task | Technical work | Progress |
|---|---|---|
| Design settings + preview | config.xml certificates fieldset; admin/src/Field/CertpreviewField.php | DONE |
| Issued-record admin | admin/src/{View,Model,Controller}/Certificate(s)*, forms/certificate.xml (course, user, number, issue/expiry date, quiz score, enabled) | DONE |
| Issue gating | AdhritlmsHelper::maybeIssueCertificate() |
DONE |
- Certificate render (self-contained inline styles)
- Download Image (PNG) / Download PDF (html2canvas + jsPDF)
- Share: native, LinkedIn / Facebook / X, copy link
- Verify page: number input, captcha, result summary (holder/course/date/number)
| Task | Technical work | Progress |
|---|---|---|
| Render + export + share | site/src/View/{Certificate,Certificates} + tmpl; html2canvas + jsPDF (CDN, lazy), off-screen capture sandbox | DONE |
| Public verification + captcha | site/src/{View,Controller,Model}/Verify*; captcha via Joomla form field; signed shared-link token | DONE |
Settings & Configuration
All component options live in one config (Components → Adhrit LMS → Options), organised into fieldsets. In 3.6.0 the former Security, Profile and Maps tabs were consolidated as titled sections inside General (same field names, same saved values), and menu-visibility switches were added.
| Field | Type | Purpose |
|---|---|---|
currency |
list | Currency code:symbol |
items_per_page |
number | Frontend page size |
quiz_pass_percentage |
number | Default pass mark % |
certificate_enabled |
radio | Master certificate switch |
show_course_rating |
radio | Show ratings |
allow_guest_preview |
radio | Allow guest preview |
nav_show_coursemgmt |
radio | Show Course Management group |
nav_show_talentmgmt |
radio | Show Talent Management group |
nav_show_eventsmgmt |
radio | Show Events group |
nav_show_ordermgmt |
radio | Show Order Management group |
captcha_enabled |
radio | (Security section) captcha on verify form |
profile_avatar |
radio | (Profile section) allow avatar upload |
google_maps_api_key |
text | (Maps section) Maps key |
map_height |
number | (Maps) map height |
map_zoom |
number | (Maps) default zoom |
| Task | Technical work | Progress |
|---|---|---|
| General fieldset | admin/config.xml [general] | DONE |
| Field | Type | Purpose |
|---|---|---|
paypal_enabled |
radio | Enable PayPal |
paypal_sandbox |
radio | Sandbox mode |
paypal_email |
PayPal account email | |
bank_transfer_enabled |
radio | Enable bank transfer |
bank_details |
textarea | Bank details shown to buyer |
upi_enabled |
radio | Enable UPI |
upi_id |
text | UPI ID (name@bank) |
upi_payee_name |
text | UPI payee name |
direct_payment_enabled |
radio | Enable direct/offline |
payment_manual_approval |
radio | Hold paid orders as Pending |
| Task | Technical work | Progress |
|---|---|---|
| Payment fieldset | admin/config.xml [payment] | DONE |
| Field | Type | Purpose |
|---|---|---|
(see E8) |
Template, borders, frame, logo, heading/body, number position, signatory, verify link, preview |
| Task | Technical work | Progress |
|---|---|---|
| Certificates fieldset | admin/config.xml [certificates] | DONE |
| Field | Type | Purpose |
|---|---|---|
course_text_color |
color | Course text colour override |
lesson_text_color |
color | Lesson text colour override |
attachments_label |
text | Rename 'Attachments' heading |
resources_label |
text | Rename 'Resources' heading |
| Task | Technical work | Progress |
|---|---|---|
| Appearance fieldset | admin/config.xml [appearance] | DONE |
| Field | Type | Purpose |
|---|---|---|
ce_text_color |
color | Editor text |
ce_chapter_color |
color | Chapter titles |
ce_lesson_color |
color | Lesson titles |
ce_accent_color |
color | Accent buttons |
ce_quiz_badge_color |
color | Quiz badge |
ce_glow |
radio | Neon glow |
| Task | Technical work | Progress |
|---|---|---|
| Ceditor fieldset | admin/config.xml [ceditor] | DONE |
| Field | Type | Purpose |
|---|---|---|
inv_logo |
media | Invoice logo |
inv_company |
text | Company name |
inv_header |
textarea | Header/address |
inv_tax_info |
text | Tax registration (GSTIN/VAT) |
inv_prefix |
text | Invoice number prefix |
inv_template |
list | classic/modern/minimal |
inv_accent |
color | Accent colour |
inv_taxes |
subform | Tax rows: label + rate % |
inv_footer |
textarea | Footer note |
inv_preview |
invoicepreview | Preview sample invoice |
| Task | Technical work | Progress |
|---|---|---|
| Invoices fieldset + fields | admin/config.xml [invoices]; admin/src/Field/InvoicepreviewField.php | DONE |
| Field | Type | Purpose |
|---|---|---|
rules |
rules | Joomla ACL rules for com_adhritlms |
| Task | Technical work | Progress |
|---|---|---|
| Permissions fieldset | admin/config.xml [permissions] | DONE |
Backup, Restore & Bulk Import
A dedicated Backup & Restore admin view (admin-only, pinned above Settings) downloads the entire LMS (all 18 tables) as one JSON and restores it in two modes; the Courses list exports/imports selected courses as JSON/XML; and Quiz Questions, Lessons and Categories each ship a CSV template with a mapped importer.
- Record-count badges per table
- Download full backup (JSON of 18 tables)
- Upload backup file
- Mode: Create new items (remapped) / Modify existing (upsert by id)
- Warning + confirm before restore
| Task | Technical work | Progress |
|---|---|---|
| Backup view + controller | admin/src/{View,Controller}/Backup*, admin/tmpl/backup/default.php — export(), restore() (FK remap, upsert) |
DONE |
| Task | Technical work | Progress |
|---|---|---|
| Course export/import | CoursesController.php export()/import() |
DONE |
| CSV importers | Quiz/Lessons/Categories controllers importcsv(); templates in media/com_adhritlms/import/ |
DONE |
Frontend Learner Experience
The public/student side: catalog and category browsing, the course page (enroll), the gated lesson player with progress tracking, checkout, the tabbed My Learning profile hub, quizzes, certificates and verification. It is template-agnostic (Bootstrap 5 utility classes, Bootstrap Icons with currentColor) and mobile-friendly.
- Search box
- Category filter, Level filter, Sort (latest/title/price)
- Card: image, title, category, price/Free, wishlist heart
- Category listing: clickable cards with course counts
| Task | Technical work | Progress |
|---|---|---|
| Catalog + category views | site/src/View/{Courses,Coursescategories,Coursescategory} + tmpl | DONE |
| Wishlist | site/src/Controller/WishlistController.php; heart on cards + course/category pages | DONE |
- Hero: title, image, price / Enroll / Buy / Enroll for Free
- What you'll learn, Requirements
- Curriculum accordion (Show all / Close all, chapter descriptions, durations, completion marks + legend + progress bar)
- Instructors block
- Reviews (avg rating + list) + Leave a Review
- Wishlist heart
| Task | Technical work | Progress |
|---|---|---|
| Course view + enroll | site/src/View/Course + tmpl; CourseController.php enroll(); exact getItemId() routing |
DONE |
| Reviews submit | frontend rating + text (gated to purchasers) | DONE |
- Media: YouTube/Vimeo/MP4 or rich text
- Course title (H3, linked) above Back-to-Course
- Downloads & Attachments (renamable) + chapter Resources (collapsed, gated)
- Mark as Complete → next / quiz
- Sidebar: chapters→lessons, completion check / current / locked, progress bar %, paperclip markers
- Mobile: floating Curriculum button → slide-in drawer
| Task | Technical work | Progress |
|---|---|---|
| Lesson player | site/src/View/{Lesson,Lessons}, site/tmpl/lesson/default.php | DONE |
| Mobile curriculum drawer | off-canvas slide-in (<768px), backdrop + Escape | DONE |
- Checkout (E7)
- My Purchases: course, date, amount, status, Continue Learning
| Task | Technical work | Progress |
|---|---|---|
| Cart/payment/purchases views | site/src/View/{Cart,Payment,Purchases} + tmpl | DONE |
- Tabs: My Courses (progress bars), Wishlist (enrol/remove), Certificates, Invoices (View buttons), Downloads (resources+attachments grouped by course), Preferences (favourite categories), Account (Joomla summary + edit link)
- Avatar upload (JPG/PNG/WebP ≤ 2 MB) gated by Settings → Profile
| Task | Technical work | Progress |
|---|---|---|
| Profile hub | site/src/{View,Model,Controller}/Profile*, site/tmpl/profile/default.php | DONE |
| Avatar + preferences | ProfileController::uploadAvatar()/savePreferences()/removeAvatar() |
DONE |
| Task | Technical work | Progress |
|---|---|---|
| Invoice view | site/src/View/Invoice + tmpl | DONE |
| 'Prev' from new tab (deferred) | link back to profile Invoices tab / window.close() when opener exists | IN PROGRESS |
REST API & Mobile
A native Joomla Web Services layer for a Joomla-based/Flutter mobile app. The catalog API is read-only JSON:API (Joomla API token). The Learner API is per-user: the app logs in for a rotating bearer token, then reads and writes its own data — all resolved from the token, never a user id in the URL, and gated by the same server-side rules as the website. Quiz answers, other users' data and admin operations are never exposed.
- GET /courses (+/{id}) — teacher_ids, chapter_count, lesson_count, review_count, rating_avg, has_final_quiz, currency, price_display
- GET /courses/{id}/curriculum — token-authenticated (X-Joomla-Token), nested chapters → lessons (is_preview, duration, type, has_quiz)
- Scoping: /courses?category=, /chapters?course=, /lessons?course=&chapter=
- /coursescategories (course_count), /chapters (lesson_count), /lessons (has_quiz + both FKs)
- /teachers (course_ids), /speakers (event_ids), /events (speaker_ids)
- All image fields returned as absolute URLs · Auth: X-Joomla-Token (curriculum is public)
| Task | Technical work | Progress |
|---|---|---|
| JSON:API controllers/views + enrichment | api/src/Controller/*Controller.php, View/*/JsonapiView.php — prepareItem adds relationships/aggregates + AdhritlmsHelper::apiMediaUrl() |
DONE |
| List filtering + speakers resource | displayList overrides (?course/?chapter/?category); new Speakers controller+view | DONE |
| Public curriculum endpoint | api/src/Controller/CatalogController.php curriculum() |
DONE |
| Route registration | plg_webservices_adhritlms/src/Extension/Adhritlms.php onBeforeApiRoute — +speakers, +curriculum | DONE |
| Robust error handling | api/src/Controller/AbstractCatalogController.php (404/400 on item lookups) + ApiBase::execute() global JSON error boundary + body/param validation | DONE |
- POST /login, /logout (X-Adhrit-Token)
- GET /me, POST /me/preferences, /me/avatar
- GET /me/courses, /me/courses/{id}/curriculum, /me/certificates, /me/invoices, /me/wishlist (+POST toggle)
- POST /courses/{id}/enroll, /lessons/{id}/complete, GET /lessons/{id}/content
- GET /quiz, POST /quiz/submit (answers hidden)
- POST /orders, GET /me/orders/{id}
| Task | Technical work | Progress |
|---|---|---|
| Auth + token base | api/src/Controller/AuthController.php, ApiBase.php (rotating SHA-256 token) | DONE |
| Me endpoints | api/src/Controller/MeController.php | DONE |
| Learning + orders | api/src/Controller/LearnController.php (server-side gating reused from site models) | ACTIVE |
X-Adhrit-Token); in addition the account must belong to the Joomla Teacher user group, and every route is scoped to the teacher's own items (created_by) — a teacher can never touch another teacher's content and can never create a category. The whole surface has an on/off switch (Settings → API Settings → Enable Teacher API); when off, all teacher/* routes return 403 and the Teacher folder is dropped from the Postman export.- Courses: GET
/teacher/courses, POST/teacher/courses, GET/PUT/DELETE/teacher/courses/{id} - Structure: POST
/teacher/chapters·/teacher/lessons·/teacher/questions(sendadhritlms_course_id); DELETE/teacher/items/{id}?type=chapter|lesson|question - Media (multipart): POST
/teacher/courses/{id}/image(fieldimage, PNG/JPG ≤500 KB) ·/teacher/courses/{id}/pdf(fieldpdf≤2 MB) - Profile: GET/PUT
/teacher/profile· GET/PUT/teacher/preferences - JSON exchange: GET
/teacher/courses/{id}/export· POST/teacher/import(export omits media links unless the admin enables it) - Auth:
X-Adhrit-Token: {{adhrit_token}}, No Auth header. SendAccept: */*(NOTapplication/json, which Joomla's API rejects with 406).
| Task | Technical work | Progress |
|---|---|---|
| Teacher API controller | api/src/Controller/TeacherController.php — group-gated + ownership-scoped; reuses TeacherportalModel |
DONE |
| Routes + on/off + Postman | routes in plg_webservices_adhritlms 1.5.0; api_teacher_enabled gate; Teacher folder in the generated Postman collection |
DONE |
| Postman testing (steps) | 1) Settings → API Settings → Download Postman collection and import it. 2) Set collection vars base_url (e.g. https://site/api/index.php) and run Learner → Login — it captures adhrit_token automatically. 3) Open the Teacher (adhrit token) folder and run any request; the X-Adhrit-Token header is already wired and Accept is */*. 4) For image/PDF uploads pick a file on the Body → form-data image/pdf row. 5) The signed-in Joomla user must be in the Teacher group or requests return 403. |
DONE |
Companion Site Modules
Three template-agnostic site modules surface LMS content in any module position. Each refuses to install without the component and reuses the component's helpers so data and routing stay consistent.
| Field | Type | Purpose |
|---|---|---|
content_type |
list | courses/categories/teachers/events/eventcategories/search |
grid |
list | 1×1 … 4×4 |
subtitle |
textarea | Line under title |
order |
list | latest/title/ordering |
catid |
sql | Course category filter |
featured_only |
radio | Featured courses only |
search_placeholder |
text | Search box placeholder |
show_image |
radio | Show card image |
show_description |
radio | Show description |
card_cta_text |
text | Card button text |
show_cta |
radio | Show footer CTA |
cta_text |
text | CTA text |
cta_menuitem |
menuitem | CTA target |
| Task | Technical work | Progress |
|---|---|---|
| Catalog module | mod_adhritlmscatalog/ — helper, dispatcher, tmpl, install guard | DONE |
| Field | Type | Purpose |
|---|---|---|
guest_courses |
sql | Hand-picked courses for guests |
hide_enrolled |
radio | Hide owned courses |
fallback_latest |
radio | Top up with latest |
grid |
list | 1×1 … 4×4 |
subtitle |
textarea | Subtitle |
show_image |
radio | Show image |
show_description |
radio | Show description |
card_cta_text |
text | Card button text |
show_cta |
radio | Footer CTA |
cta_text |
text | CTA text |
cta_menuitem |
menuitem | CTA target |
| Task | Technical work | Progress |
|---|---|---|
| Recommended module | mod_adhritlmsrecommended/ — AdhritlmsRecommendedHelper.php, tmpl (never cached) | DONE |
| Field | Type | Purpose |
|---|---|---|
source |
list | courses/featured/recommended/events/speakers/teachers/articles/custom |
limit |
number | Slide count |
catid |
sql | Course category |
article_catid |
category | Article category |
slides |
subform | Custom slides — each row: title, subtitle, bg_type (image/color), image, bg_color, video_url, icon (Bootstrap icon), icon_size, thumb (custom bullet thumbnail), menuitem, url, cta_text, caption. Every per-slide extra is optional — empty keeps the default look |
| Task | Technical work | Progress |
|---|---|---|
| Content providers | mod_adhritlmscarousel/src/Helper/AdhritlmsCarouselHelper.php getSlides() — normalised slide shape incl. thumb/bg_type/bg_color/icon/icon_size |
DONE |
| Robust images (missing/renamed never breaks) | native lazy-loading (Owl lazyLoad off) + per-image onerror → accent placeholder; load/error refresh; visibility safety-net. tmpl/default.php |
DONE |
| Field | Type | Purpose |
|---|---|---|
mode |
list | banner / slim / small |
style_preset |
list | 20 style presets |
animation |
list | 20 transitions (10 animate.css + 10 module keyframes) |
banner_width |
list | classic / full |
banner_height |
number | Banner height px |
items_desktop/tablet/mobile |
number | Slim items per row |
small_fit |
list | 9 tile fits |
tile_size |
number | Small tile px |
tile_gap |
number | Tile gap px |
space_top |
number | Top margin px (negative pulls up to remove template whitespace) |
space_bottom |
number | Bottom margin px |
pad_x |
number | Side padding px |
| Task | Technical work | Progress |
|---|---|---|
| Modes, presets, animations | tmpl/default.php (.alc-mode/.alc-style1..20/.alc-fit CSS; Owl animateIn/Out + custom @keyframes) | DONE |
| First-load anti-scatter + spacing | window.load init, anti-FOUC opacity guard, refresh triggers; space_top/bottom + pad_x; max-width containment | DONE |
| Field | Type | Purpose |
|---|---|---|
autoplay |
radio | Autoplay |
interval |
number | Seconds |
speed |
number | Transition ms |
smart_speed |
radio | Auto-tune speed |
loop |
radio | Loop |
hover_pause |
radio | Pause on hover |
arrows |
radio | Show arrows |
arrow_style |
list | 20 arrow styles (Bootstrap Icons glyphs, responsive) |
arrow_size |
number | Arrow px |
dots |
list | 11 clickable bullet types: none/dots/dashes/squares/pills/rings/bars/numbers/roman/alpha/thumbs/icons |
dots_orientation |
list | horizontal / vertical |
dots_position |
list | below + 8 overlay spots (incl. left-center / right-center) |
dots_size |
number | Bullet/thumb px |
lazyload |
radio | Lazy-load images |
| Task | Technical work | Progress |
|---|---|---|
| 20 arrow styles | tmpl/default.php .alc-arr-1..20; navText Bootstrap Icons glyphs (.alc-gi*); mobile shrink media query | DONE |
| Custom clickable pagination | own <button> per slide wired via to.owl.carousel; active sync via relative() on changed.owl.carousel — every bullet type navigates (fixes inert numbers/letters/thumbs and the single-stretched-dot bug) |
DONE |
| Bullet orientation + positions | dots_orientation horizontal/vertical; .alc-dotspos-* incl. left-center / right-center rails | DONE |
| Autoplay progress bar | template-coloured .alc-progress synced to autoplayTimeout (planned) | IN PROGRESS |
| Field | Type | Purpose |
|---|---|---|
google_font |
text | Google font family |
title_font/size/color |
mixed | Title typography |
subtitle_font/size/color |
mixed | Subtitle typography |
caption_font/size/color |
mixed | Caption typography |
text_align |
list | left/center/right |
show_button |
radio | Show button |
button_text |
text | Text |
button_menuitem |
menuitem | Fallback link |
button_position |
list | inherit/left/center/right |
button_style |
list | solid/outline/pill/link |
button_icon |
text | Bootstrap icon |
button_font/size/color/bg |
mixed | Button typography & colours |
label_free |
text | Rename 'Free' |
label_featured |
text | Rename 'Featured' |
accent_color |
color | Accent (defaults to template primary) |
| Task | Technical work | Progress |
|---|---|---|
| Typography/button/labels | mod_adhritlmscarousel.xml + tmpl/default.php (--alc-* variables) | DONE |
Security Scan Report
Point-in-time results of a full security audit of the Adhrit LMS suite (component, three modules and the Web Services plugin) covering SQL injection, XSS, CSRF, broken access control / IDOR, file-upload & path-traversal, authentication & token handling, SSRF / open redirect and unsafe PHP. Method: four parallel source-level audits plus an independent verification pass that re-checked every claim against the source. Outcome: 0 Critical, 0 High, 0 Medium; 5 Low-severity items found and fixed in 4.2.17. The user stories below record each area that was Verified secure (no action needed). This section is a snapshot — it is only regenerated when the scan is re-run.
Every database query in the component, modules and plugin was reviewed for untrusted input reaching SQL.
Acceptance (no action needed):
- All identifiers pass through
$db->quoteName()and all values through$db->quote()or an(int)cast. - No string interpolation of request data into query fragments; the query builder is used throughout.
- Learner/teacher endpoints resolve numeric route ids via
(int)casts only.
Scan result:
[SQLi] scanned: admin/src, site/src, api/src, plg_webservices, modules
[SQLi] raw-concatenation sinks found ....... 0
[SQLi] quoteName()/quote()/(int) coverage .. 100%
RESULT: PASS - no injectable query paths| Task | Technical work | Progress |
|---|---|---|
| Verified: parameterised queries only | Query builder + quoteName/quote/(int) across all apps |
DONE |
Frontend templates were reviewed for unescaped output of user/teacher-authored data. Plain values are escaped with htmlspecialchars(..., ENT_QUOTES); rich-text (course/lesson/quiz HTML) is now additionally passed through a server-side HTML filter on output.
Acceptance (no action needed):
- Scalar fields are escaped at the point of output.
- Rich-text is filtered through
AdhritlmsHelper::safeHtml()(Joomla InputFilter, xssAuto) which strips<script>,<iframe>andon*handlers while keeping formatting. - The
event_typefallback is HTML-escaped.
Scan result:
[XSS] templates reviewed ................... 40+
[XSS] unescaped scalar echoes ............... 0
[XSS] rich-text now filtered via safeHtml() . course, lesson, quiz, event, teacher, speaker
RESULT: PASS - output-filtering hardening added in 4.3.0| Task | Technical work | Progress |
|---|---|---|
| Verified + hardened: output filtering | safeHtml() applied to rich-text templates; scalars escaped |
DONE |
All state-changing frontend actions were checked for anti-CSRF token enforcement.
Acceptance (no action needed):
- Every mutating controller task calls
checkToken(); forms emitHTMLHelper::_('form.token'). - The teacher portal's GET JSON export still requires a valid form token in the query string.
- No state change is reachable by a simple cross-site GET.
Scan result:
[CSRF] mutating controller tasks .......... all guarded by checkToken()
[CSRF] unguarded state changes ............. 0
RESULT: PASS| Task | Technical work | Progress |
|---|---|---|
| Verified: CSRF tokens enforced | checkToken() on every write task |
DONE |
Object references were checked to ensure a user cannot read or mutate another user's data by changing an id.
Acceptance (no action needed):
- Progress/quiz endpoints gate on course enrolment (
userHasAccess()). - Teacher portal + teacher API scope every read/edit/delete to
created_by = current user. - The Adhrit token is never derived from a client-supplied user id.
Scan result:
[IDOR] learner endpoints - enrolment gate .. enforced
[IDOR] teacher endpoints - ownership gate ... enforced (created_by)
[IDOR] horizontal access attempts ........... blocked
RESULT: PASS| Task | Technical work | Progress |
|---|---|---|
| Verified: ownership + enrolment gates | enrolment gate (4.2.17) + teacher ownership (4.3.0) | DONE |
Every upload path was reviewed for type confusion, oversized files and directory traversal.
Acceptance (no action needed):
- Uploads validate
is_uploaded_file+ extension allow-list + MIME + (images)getimagesize(). - Sizes are capped by admin config: profile 600 KB, teacher image 600 KB, teacher PDF 2 MB.
- Teacher files are confined to a per-username folder; stored names are randomised.
Scan result:
[UPLOAD] validators: is_uploaded_file+ext+mime+getimagesize .. present
[UPLOAD] size caps (config) ............ avatar 600KB / img 600KB / pdf 2MB
[TRAVERSAL] path confinement ........... images/adhritlms/teachers//
RESULT: PASS| Task | Technical work | Progress |
|---|---|---|
| Verified + hardened: governed uploads | size caps + per-teacher confinement added in 4.3.0 | DONE |
The API authentication scheme and password handling were reviewed.
Acceptance (no action needed):
- Passwords verified with
UserHelper::verifyPassword(bcrypt); constant-shape failures avoid account enumeration. - API token =
base64(userId:secret); onlysha256(secret)is stored, compared withhash_equals; rotates on login, cleared on logout. - Failed API logins are now rate-limited per IP + username (added 4.3.0).
Scan result:
[AUTH] password verify ......... bcrypt (verifyPassword)
[AUTH] token at rest ........... sha256, hash_equals compare
[AUTH] brute-force throttle .... ENABLED (configurable) - new in 4.3.0
RESULT: PASS| Task | Technical work | Progress |
|---|---|---|
| Verified + hardened: token auth + throttle | login rate-limiting added in 4.3.0 | DONE |
Outbound requests and redirect targets were reviewed for attacker control.
Acceptance (no action needed):
- Post-action redirects validate the
returntarget withUri::isInternal()before redirecting (fixed in 4.2.17). - No user-controlled URL is fetched server-side; map/embeds are built from rawurlencoded values.
- External profile links are normalised through
externalUrl().
Scan result:
[REDIRECT] return-target validation .. Uri::isInternal() (fixed 4.2.17)
[SSRF] server-side fetch of user URLs . none
RESULT: PASS| Task | Technical work | Progress |
|---|---|---|
| Verified + fixed: open-redirect guards | Uri::isInternal() on all return redirects (4.2.17) |
DONE |
The code was scanned for dangerous sinks (eval, dynamic includes, command execution, weak randomness).
Acceptance (no action needed):
- No
eval(),system(),exec(),shell_exec()or user-controlledinclude. - Security-relevant identifiers use
random_bytes()(certificate numbers moved offmd5(microtime())in 4.2.17). - No unserialisation of untrusted input.
Scan result:
[PHP] eval/exec/system/shell_exec ..... 0
[PHP] dynamic include of request data .. 0
[PHP] weak randomness for secrets ...... 0 (random_bytes used)
RESULT: PASS| Task | Technical work | Progress |
|---|---|---|
| Verified + fixed: no unsafe sinks | certificate number → random_bytes() (4.2.17) |
DONE |
Five low-severity items surfaced by the audit were fixed before this snapshot.
Acceptance (no action needed):
- Open redirect via
returnparam in Lessons/Wishlist controllers — validated withUri::isInternal(). - Missing enrolment gate on lesson/quiz progress endpoints — gated with
userHasAccess(). - Unescaped
event_typefallback — HTML-escaped. - Predictable certificate numbers — switched to
random_bytes().
Scan result:
[FIX] LOW open-redirect (Lessons, Wishlist) ...... FIXED 4.2.17
[FIX] LOW enrolment gate (Lessons, Quiz, Course) .. FIXED 4.2.17
[FIX] LOW event_type output escaping ............. FIXED 4.2.17
[FIX] LOW certificate number randomness ......... FIXED 4.2.17
Remaining Critical/High/Medium: 0| Task | Technical work | Progress |
|---|---|---|
| Fixed: 5 low-severity items | see component 4.2.17 changelog | DONE |
Teacher Portal & Media Governance
A frontend portal for members of the Joomla Teacher user group (created on install) to author and manage their own courses, plus a matching Teacher REST API for the mobile app. Teachers work only on items they own (created_by), upload only into their own per-username media folder under governed size/type limits, and can never create course categories. Which course fields are editable, whether teachers may delete, and whether JSON exports include media links are all admin-configurable.
Teacher user group (child of Registered). Membership unlocks the portal and the teacher API; the component resolves the group at runtime.| Task | Technical work | Progress |
|---|---|---|
| Teacher group + ownership columns | script.php ensureTeacherGroup(); created_by back-filled on chapters/lessons/quizzes/questions; teachers.user_id link |
DONE |
| Identity + ownership helpers | AdhritlmsHelper::isTeacher()/teacherOwns()/teacherCanDelete() |
DONE |
- Dashboard: own courses, publish state, JSON import
- Course form: title (always) + admin-selected fields; existing-category picker (no create)
- Content editor: chapters, lessons (with chapter + description), quiz questions
| Task | Technical work | Progress |
|---|---|---|
| Portal MVC | site/src/Controller/TeacherController.php, View/Teacherportal, Model/TeacherportalModel.php, tmpl/teacherportal/* | DONE |
| Configurable editable fields | teacher_editor_fields checkboxes in Settings → Teacher Portal |
DONE |
| Field | Type | Purpose |
|---|---|---|
profile_avatar_max_kb |
number | Max profile picture size (default 600 KB) |
teacher_img_max_kb |
number | Max teacher course image (default 600 KB) |
teacher_pdf_max_mb |
number | Max teacher course PDF (default 2 MB) |
| Task | Technical work | Progress |
|---|---|---|
| Governed upload helpers | teacherUploadImage()/teacherUploadPdf() → images/adhritlms/teachers/<slug>/ |
DONE |
| Profile 600KB cap | ProfileController::uploadAvatar honours profile_avatar_max_kb |
DONE |
| Task | Technical work | Progress |
|---|---|---|
| Ownership enforcement | every mutate checks created_by; delete gated by teacher_can_delete |
DONE |
| Own profile + preferences | saveProfile() (linked teachers.user_id) + savePreferences() |
DONE |
GET/POST /teacher/courses,GET/PUT/DELETE /teacher/courses/:idPOST /teacher/chapters|lessons|questions,DELETE /teacher/items/:idPOST /teacher/courses/:id/image|pdf,GET /teacher/courses/:id/export,POST /teacher/importGET/PUT /teacher/profile,GET/PUT /teacher/preferences
| Task | Technical work | Progress |
|---|---|---|
| API controller + routes | api/src/Controller/TeacherController.php; routes in plg_webservices_adhritlms 1.5.0 | DONE |
| Task | Technical work | Progress |
|---|---|---|
| Export/import | TeacherportalModel::exportCourse()/importCourse(); media stripped unless teacher_json_export_media |
DONE |
| Import toggle | teacher_json_import master switch |
DONE |
- Left panel: SortableJS tree; drag lessons across chapters; reorder chapters/lessons
- Right panel: click a chapter/lesson to edit it in place
- Dashboard: card grid + live search + status/pricing filters + empty-state guide
| Task | Technical work | Progress |
|---|---|---|
| Two-pane builder + auto-save | tmpl/teacherportal/editor.php (SortableJS via cdnjs) → teacher.reorder → TeacherportalModel::reorder() |
DONE |
| Editor 500 fixed | removed HTMLHelper::_('editor.editor') dependency; plain filtered field |
DONE |
| Field | Type | Purpose |
|---|---|---|
teacher_profile_avatar |
radio | Admin on/off for teacher profile pictures |
teacher_show_email / show_email |
radio / column | Global + per-teacher email visibility |
teacher_show_website / show_website |
radio / column | Global + per-teacher website visibility |
| Task | Technical work | Progress |
|---|---|---|
| Profile media + validation | teacherUploadImage()/teacherUploadSignature(); validateSocialUrl() |
DONE |
| Public display | clickable website under email on tmpl/teacher/default.php | DONE |
| Task | Technical work | Progress |
|---|---|---|
| Teacher API toggle | api_teacher_enabled + apiTeacherEnabled() gate; Postman folder gated |
DONE |
| Field | Type | Purpose |
|---|---|---|
teacher_lesson_fields |
checkboxes | Which lesson fields show: type / video / preview / attachments |
teacher_chapter_resources |
radio | Enable chapter description + resources |
teacher_lesson_rte / teacher_lesson_media |
radio | WYSIWYG on/off and whether the media selector is allowed |
| Task | Technical work | Progress |
|---|---|---|
| Editor fields + hierarchy | tmpl/teacherportal/editor.php; lessonFields()/normaliseResources(); question chapter+lesson selectors |
DONE |
| Controlled TinyMCE | cdnjs GPL build, lazy init, media/paste gated by teacher_lesson_media |
DONE |
| Task | Technical work | Progress |
|---|---|---|
| Adopt existing record | profile() matches created_by/email/name and claims via user_id |
DONE |
| specialist_in + backend signature | portal field + signature_image media field on the admin Teacher form |
DONE |
Issues & Fixes Log
| ID | Ver | Epic / Area | Issue / Requirement | Technical work | Status |
|---|---|---|---|---|---|
| I-0v7a | 4.7.0 | E10/E15 Import | Course import (with its chapters, lessons and quiz) needed an explicit choice between overwriting the existing course and importing a fresh copy with new IDs; the teacher import was also dropping the chapter/lesson relationships. | New import_mode (new / overwrite) on the admin Courses import and the teacher-portal import (+ mode on POST /teacher/import). Exports now carry reference IDs (_id/_ref/_chapter/_lesson); import re-maps chapter→lesson→quiz so the copy is fully linked. Overwrite updates the course by ID and replaces its content; teachers may overwrite only courses they own. Component 4.7.0. |
DONE |
| I-0v6a | 4.6.0 | E15 Uploads | Teacher profile/signature/course-image uploads crashed with Class "Joomla\CMS\Filesystem\Folder" not found on newer Joomla; image type check needed tightening. |
Folder creation switched to native mkdir + a blank index.html per teacher folder (learner avatar too); teacher images limited to PNG/JPG-JPEG (ext + MIME), 500 KB. Component 4.6.0. |
DONE |
| I-0v6b | 4.6.0 | E12 API / Postman | The exported Postman collection returned 406 (Could not match accept header) on teacher/learner GETs because it sent Accept: application/json. |
Adhrit-token requests in the export now send Accept: */*, which Joomla's API app negotiates cleanly; documented for mobile clients. Component 4.6.0. |
DONE |
| I-0v6c | 4.6.0 | E15 Quiz authoring | The frontend question editor lacked answer choices/correct-answer entry, chapter/lesson binding cues, and editing of existing questions. | Choices repeater + correct selector (radio/checkbox/text) building list_answers/correct_answer; tree selection binds question chapter/lesson with icons; existing questions listed per selection with edit/delete. Component 4.6.0. |
DONE |
| I-0v6d | 4.6.0 | E8 Certificates | Certificate: empty {course}, off-centre signatures/overlap, cramped long course names, few styles/borders, plain-only border in export, weak background. | {course} resolved from the course id; signatures centred over their line with independent L/C/R positions; wider body; 12 templates + 8 border styles + optional inline-SVG vector flourishes (export-safe). Component 4.6.0. | DONE |
| I-0v6e | 4.6.0 | E2 Dashboard / E13 Carousel | Admin needed a pending-review course list; carousel needed glow + highlight typography. | Dashboard 'Courses pending review' panel (chapters/category/author). Carousel 1.6.0: per-element outer glow (colour+size) and text highlight background with transparency (Word-style). | DONE |
| I-0v5a | 4.5.0 | E15 Teacher profile | Editing the teacher profile in the portal produced a blank public profile (a duplicate teacher record was created), and Specialist In was not editable. |
TeacherportalModel::profile() now adopts an existing backend teacher row (matched by created_by/email/name) and claims it via user_id instead of creating a second record, so portal + public page share one profile; specialist_in added to the portal form/save. Component 4.5.0. |
DONE |
| I-0v5b | 4.5.0 | E15 Chapter/Lesson authoring | The frontend editor was missing chapter description + resources, and lesson type / video / free-preview / attachments, and questions didn't follow the backend chapter→lesson hierarchy. | Chapter description + resources (gated by teacher_chapter_resources); lesson lesson_type/video_*/is_preview/attachments gated by teacher_lesson_fields; question form gained chapter+lesson selectors with the lesson list filtered by chapter. Model saveChild() extended + normaliseResources(). Component 4.5.0. |
DONE |
| I-0v5c | 4.5.0 | E15 Rich-text editor | Lessons needed a controlled rich-text editor whose media options can be limited from the backend. | Controlled TinyMCE (cdnjs, GPL) lazily initialised in the lesson editor; media plugins/toolbar and paste_data_images disabled unless teacher_lesson_media is on; teacher_lesson_rte switches between WYSIWYG and a plain box, with a safe fallback. Component 4.5.0. |
DONE |
| I-0v5d | 4.5.0 | E6/E8 Teacher signature | Admins needed to upload a teacher signature from the backend into the teacher's folder. | signature_image media field added to the admin Teacher form (directory images/adhritlms/teachers); rendered on certificates per the 4.4.0 teacher-signature toggles. Component 4.5.0. |
DONE |
| I-001 | 4.4.0 | E15 Course editor | Frontend course editor returned a 500 (Joomla\CMS\HTML\HTMLHelper editor not found) and needed a proper drag-and-drop builder. |
Removed the WYSIWYG dependency (plain filtered field); rebuilt tmpl/teacherportal/editor.php as a two-pane builder — SortableJS structure tree (drag lessons between chapters, reorder chapters/lessons, auto-save via teacher.reorder) + click-to-edit forms. Dashboard now shows catalog-style cards with search/status/pricing filters and an empty-state guide. Component 4.4.0. |
DONE |
| I-002 | 4.4.0 | E12/E15 API | Teacher API had no on/off switch; Postman export and X-Adhrit-Token handling needed confirming. | New api_teacher_enabled (Settings → API Settings); apiTeacherEnabled() gate in the API teacher controller; Postman export omits the Teacher folder when off and includes all teacher endpoints when on; teacher endpoints validate the same X-Adhrit-Token. Component 4.4.0. |
DONE |
| I-003 | 4.4.0 | E15 Teacher profile | Teachers needed a profile picture with an admin on/off, validated social links, and admin+per-teacher control over showing email and website (website clickable under the email). | teacher_profile_avatar toggle + upload; validateSocialUrl() drops mismatched Facebook/X/LinkedIn links; global teacher_show_email/teacher_show_website + per-teacher show_email/show_website columns; public teacher page shows website as a clickable link under the email. Component 4.4.0. |
DONE |
| I-003b | 4.4.0 | E8 Certificates | Certificate download was cropped again; teachers should be able to add their own signature, with admin control over teacher/site signatures and their placement. | Enlarged html2canvas capture padding (no more edge crop); teacher signature_image upload (max 500 KB); config cert_show_site_sign/cert_site_sign_position and cert_show_teacher_sign/cert_teacher_sign_position (left/center/right) wired into renderCertificate(). Component 4.4.0. |
DONE |
| I-004 | 4.3.0 | E15 Teacher Portal | Provide a frontend portal so Teacher-group users can author and manage their own courses (chapters, lessons, quizzes), edit only their own items, delete only when the admin allows it, and set course preferences like a learner — without ever being able to create course categories. | New site MVC TeacherController + Teacherportal view/model/templates; Joomla Teacher user group created on install; ownership via created_by; admin-configurable editable fields and delete policy. Component 4.3.0. |
DONE |
| I-005 | 4.3.0 | E15 Teacher API | Expose the teacher portal to the mobile app. | New api/src/Controller/TeacherController.php (Adhrit-token, Teacher-gated, ownership-scoped) with courses/chapters/lessons/questions/profile/preferences/upload/import-export routes. plg_webservices_adhritlms 1.5.0. | DONE |
| I-006 | 4.3.0 | E15 Media governance | Profile pictures must be ≤600 KB; teacher course images ≤600 KB and PDFs ≤2 MB (all admin-configurable); teacher uploads must stay in the teacher's own folder. | Config profile_avatar_max_kb / teacher_img_max_kb / teacher_pdf_max_mb; teacherUploadImage()/teacherUploadPdf() confine to images/adhritlms/teachers/<slug>/. Component 4.3.0. |
DONE |
| I-007 | 4.3.0 | E15 JSON exchange | Teachers can import a course from JSON and export their own course; a teacher's export must omit image/download links unless the admin enables it. | TeacherportalModel::importCourse()/exportCourse(); media stripped unless teacher_json_export_media; import gated by teacher_json_import. Component 4.3.0. |
DONE |
| I-008 | 4.3.0 | E14 Security | Implement the audit's recommended hardening. | API login rate-limiting (#__adhritlms_login_throttle, configurable), optional HSTS header when HTTPS is enforced, captcha now fails closed on backend error, and rich-text output filtering via AdhritlmsHelper::safeHtml(). Component 4.3.0. |
DONE |
| I-020 | 4.2.16 | E12 API | The Require-HTTPS enforcement let an HTTP request through when the site auto-redirects HTTP→HTTPS (the client follows the redirect, so PHP only ever sees the upgraded HTTPS request), and it could mis-detect HTTPS behind a TLS-terminating proxy/load balancer. | HTTPS detection is now proxy-aware (AdhritlmsHelper::apiIsHttps()): honours X-Forwarded-Proto / X-Forwarded-Ssl / port 443 in addition to a direct TLS connection. Documented that true enforcement belongs at the web server (redirect + HSTS, ideally reject HTTP on /api) since an app-level check cannot see a request the server has already redirected — the app toggle is a backstop. See the separate API Integration Guide. Component 4.2.16. |
DONE |
| I-030 | 4.2.15 | E9 Settings / E12 API | Admins needed control over the REST API: see whether the Web Services plugin is installed/enabled and its version, turn the Catalog and Learner APIs on/off independently, set a token lifetime, force HTTPS, and export all endpoints to Postman. | New Settings → API Settings tab (admin/config.xml [apisettings]): plugin status/version field (admin/src/Field/ApistatusField.php), Catalog/Learner enable toggles, token lifetime (days; 0 = never), Require-HTTPS switch, and a Postman export button (PostmanexportField.php → admin/src/Controller/ApiController.php postman() streams a v2.1.0 collection built by AdhritlmsHelper::postmanCollection()). Enforcement: AdhritlmsHelper::apiBlock() gates every request (HTTPS + surface toggle) via ApiBase::execute() and the catalog controllers; token TTL checked in authUser() (issue time stored at login, cleared at logout). Component 4.2.15. |
DONE |
| I-040 | 4.2.13 | E12 API | Path {id} routes failed: POST /me/wishlist/6 reached the controller but returned Course id is required (422), because the router's :id variable wasn't landing in the input the way the custom controllers read it. This affected every id-in-path route (wishlist toggle, enroll, lesson content/complete, order status, curriculum, catalog items). |
Added AdhritlmsHelper::apiRouteId() which reads the input id and, if empty, falls back to the last full numeric segment of the request path (e.g. /me/wishlist/6 → 6, /me/courses/12/curriculum → 12). All custom controllers now resolve the route id via ApiBase::routeId(), and the catalog item controller uses the same helper. Component 4.2.13. |
DONE |
| I-045 | 4.2.10 | E12 API | Several authenticated endpoints — GET /me/courses, /me/certificates, /me/invoices, /me/wishlist and GET /quiz — returned a 403 Forbidden (Joomla's HTML page, before the controller ran) with a valid X-Adhrit-Token, while /me and /me/preferences worked. There was also no endpoint to list all of a learner's orders, and users suspected the token was expiring. |
The failing endpoints were the ones built on Site models (getModel('Profile','Site'), getModel('Quiz','Site')), which do not resolve cleanly under the Joomla API application; the working ones used direct DB access. Rewrote me.courses, me.certificates, me.invoices, me.wishlist, learn.quiz and learn.orderStatus to query the database directly (self-contained), and routed the genuinely reused logic (quiz/submit scoring, orders creation/coupons) through the component's own MVCFactory via a new ApiBase::siteModel() so web/app never drift. Added GET /v1/adhritlms/me/orders (full order list). Confirmed the bearer token does not time-expire — it is valid until the next login (rotates it) or logout (revokes it); the 403 was never expiry. Files: api/src/Controller/MeController.php, LearnController.php, ApiBase.php, plg_webservices_adhritlms/src/Extension/Adhritlms.php. Component 4.2.10 / plugin 1.3.0 / package 4.2.10. |
DONE |
| I-050 | 4.2.9 | E12 API | Error handling was inconsistent across the REST API. Catalog item endpoints (/courses/{id}, /chapters/{id}, /lessons/{id}, /coursescategories/{id}, /teachers/{id}, /speakers/{id}, /events/{id}) returned a raw 500 when the id did not exist instead of a 404, and POST /me/preferences could hang / time out on a wrong body. Some endpoints already returned proper errors, but not all — every endpoint needed to answer with a clear status and message (record-not-found, bad request body, update failure). |
Added a shared api/src/Controller/AbstractCatalogController.php that all seven catalog controllers extend: displayItem() verifies the row exists (lightweight COUNT on the PK) and returns a clean JSON 404 ({success:false,error}) for a missing id and 400 for a non-numeric/zero id; both displayList() and displayItem() wrap the parent call so any unexpected failure becomes a JSON error with the right status, never a blank 500. Added a global error boundary in ApiBase::execute() so every learner endpoint (auth/me/learn/catalog-curriculum) converts any uncaught Throwable into a clean JSON error instead of hanging. Tightened validation: /me/preferences 400s on a missing/non-array categories (the timeout case), quiz/submit validates answers, and enroll / lesson / complete / orders / me/orders/{id} reject a missing/zero id with 400. Component 4.2.9 / package 4.2.9. |
DONE |
| I-055 | 4.2.8 | E12 API | The new course-curriculum endpoint had been registered as public (open to anyone), which exposed a course's chapter/lesson structure without any credential. It should be secured with Joomla authentication like the rest of the catalog so only token-holding clients can read it. | Re-registered GET /v1/adhritlms/courses/{id}/curriculum with public => false so the Joomla API application enforces a valid X-Joomla-Token before the controller runs (course enrolment is still NOT required — it is catalog/preview data, just credential-gated). plg_webservices_adhritlms/src/Extension/Adhritlms.php; docblocks updated in api/src/Controller/CatalogController.php. Component 4.2.8 / plugin 1.2.1 / package 4.2.8. |
DONE |
| I-060 | 4.2.7 | E12 API | A mobile developer reported the catalog could not connect the datapoints: GET /courses/{id} returned no chapter or lesson ids, there was no way to fetch just one course's chapters/lessons, teacher/speaker relationships were absent, images came back as relative paths, and there was no single call to render a course's curriculum for a detail/preview screen. In short, the resources existed but nothing tied them together for an app. |
Enriched the whole catalog layer: (1) new public GET /courses/{id}/curriculum (api/src/Controller/CatalogController.php) returns the course with nested chapters → ordered lessons (is_preview, duration, type, has_quiz) in one call, no enrolment; (2) list filtering ?category / ?course / ?chapter on courses/chapters/lessons via displayList overrides that seed the models' existing filter state; (3) relationship fields — courses expose teacher_ids, teachers course_ids, events speaker_ids, plus a new /speakers JSON:API resource with event_ids; (4) aggregates on courses (chapter_count, lesson_count, review_count, rating_avg, has_final_quiz, currency, price_display), categories (course_count), chapters (lesson_count) and lessons (has_quiz); (5) all catalog image fields resolved to absolute URLs via AdhritlmsHelper::apiMediaUrl(). Files: api/src/View/*/JsonapiView.php, api/src/Controller/*Controller.php, plg_webservices_adhritlms/src/Extension/Adhritlms.php. Shipped in component 4.2.7 / plugin 1.2.0 / package 4.2.7. |
ACTIVE |
| I-070 | 4.2.6 | E13 Carousel | A course feature image was renamed on the server after the course had been saved, so its URL began returning 404. Instead of that single slide degrading, the entire carousel went blank — every slide, including the ones whose images were perfectly valid. Re-adding the image to the course did not fix it (the browser had cached the 404 and the loader stayed stuck). Removing the image from the course made only that one image-less slide appear while the rest stayed blank. Expected: a missing or renamed image must never break the carousel — the affected slide should fall back gracefully and every other slide must still render and rotate. | Root cause: the module used Owl Carousel's built-in lazy-loader, which keeps every lazy image at opacity:0 until it individually fires a load event; a 404 image never fires load, so Owl's loader stalled and never revealed any of the remaining lazy images.Fix: (1) disabled Owl lazyLoad ( lazyLoad:false) and removed the owl-lazy/data-src markup; (2) slides now render a normal <img> with native browser lazy-loading (loading="lazy|eager"), so each image loads independently and one failure cannot affect the others; (3) added an onerror handler on every slide image that clears the broken src and swaps in the accent-gradient placeholder class, so a missing/renamed file degrades to the styled placeholder at the correct height; (4) added the same onerror fallback to thumbnail bullets; (5) attached a refresh to each image's load AND error event so the stage re-measures either way; (6) added a 1200 ms visibility safety-net that force-adds owl-loaded, so the carousel can never stay hidden behind the anti-flash opacity rule if an asset stalls. File: mod_adhritlmscarousel/tmpl/default.php. Shipped in module 1.5.1 / package 4.2.6. |
DONE |
| I-080 | 4.2.5 | E13 Carousel | Only the line/dash bullet was clickable; dots showed a single stretched dot; numbers, letters and thumbnails were inert. | Replaced Owl native dots with custom-rendered <button> per slide wired via to.owl.carousel; active sync via relative(); every bullet type now navigates | DONE |
| I-081 | 4.2.5 | E13 Carousel | Too few bullet styles; no orientation control. | 11 bullet types (dots/dashes/squares/pills/rings/bars/numbers/roman/alpha/thumbs/icons) + dots_orientation (horizontal/vertical) + left-center/right-center overlay positions | DONE |
| I-082 | 4.2.5 | E13 Carousel | Needed 20 responsive, Bootstrap-compatible arrow shapes (was 15). | 5 new arrow presets (solid accent circle, caret tab, double-chevron, docked bottom-right, tall edge bar) using Bootstrap Icons glyphs; responsive shrink on small screens | DONE |
| I-083 | 4.2.5 | E13 Carousel | Custom slides needed colour background, an icon, and a custom thumbnail; keep defaults when empty. | Per-slide bg_type (image/color), bg_color, icon + icon_size, thumb; helper carries the shape, template renders colour/icon and thumbnail bullets, all optional | DONE |
| I-090 | 4.2.4 | E13 Carousel | Tiles/slides scattered on first load, correcting only on refresh (esp. bottom position). | Init on window.load (not DOM-ready) + anti-FOUC opacity guard + post-load/image refresh triggers; max-width containment | DONE |
| I-091 | 4.2.4 | E13 Carousel | Bullets not clickable — the full-slide content overlay intercepted clicks. | pointer-events:none on the content overlay, auto on its own links/buttons | DONE |
| I-092 | 4.2.4 | E13 Carousel | Dash bullets merged into one line; too much top whitespace; wanted more presets/animations. | One distinct dash per slide with gaps; spacing controls (space_top negative/space_bottom/pad_x); +10 presets (20) and +10 animations (20) | DONE |
| I-100 | 4.2.3 | E13 Carousel | Small layout needed sub-layouts + tile size; users could not choose tile shape/behaviour. | 9 tile fits (square/autowidth/wide/portrait/circle/rounded/polaroid/center/stage) + tile_size/gap; Owl autoWidth + .alc-fit-* CSS | DONE |
| I-101 | 4.2.3 | E13 Carousel | Banner had no edge-to-edge option. | banner_width classic/full; .alc-bw-full 100vw breakout | DONE |
| I-102 | 4.2.3 | E13 Carousel | Only dots/thumbnails; bullets fixed below the carousel. | 5 bullet types (dots/dashes/numbers/alpha/thumbs) + dots_position overlay (6 spots) + dots_size; Owl dotsData | DONE |
| I-103 | 4.2.3 | E13 Carousel | Arrows had no style choice. | 15 arrow styles + arrow_size + on/off; .alc-arr-1..15 CSS | DONE |
| I-110 | 4.2.2 | E13 Carousel | Style preset stuck on #1 regardless of selection. | Field 'style' collided with Joomla module-chrome params['style']; renamed to style_preset | DONE |
| I-111 | 4.2.2 | E13 Carousel | Small tiles were oversized. | Owl autoWidth + fixed tile_size/gap so tiles are exactly the set size | DONE |
| I-120 | 4.2.1 | E13 Carousel | Style presets not visibly applying (esp. imageless slides). | Presets restyle slide chrome; imageless slides accent-tinted | DONE |
| I-121 | 4.2.1 | E13 Carousel | Only banner/slim; needed a dense tile layout. | New 'Small' layout type | DONE |
| I-122 | 4.2.1 | E13 Carousel | Too few animations (6). | Expanded to 10 transitions | DONE |
| I-130 | 4.2.0 | E13 Carousel | No carousel/slider module existed. | New mod_adhritlmscarousel: banner/slim, 10 presets, 6 animations, sources, typography/button, RTL | DONE |
| I-140 | 4.1.0 | E12 API | Mobile app could not access learner data (profile/progress/enroll). | Full Learner API: login/token, me/*, enroll, lesson complete, quiz get/submit, orders, invoices, certificates, wishlist | DONE |
| I-141 | 4.1.0 | E7 Commerce | PayPal never completed orders (IPN endpoint missing); checkout passed coupon code as price. | Implemented ipn() with postback verify + receiver/amount checks; paypalReturn(); fixed coupon-as-price | DONE |
| I-142 | 4.1.0 | E7 Commerce | UPI was static text; no way to actually pay. | upi://pay deep-link button + QR (qrcode.js) + transaction-ref capture; INR amount embedded | DONE |
| I-150 | 4.0.0 | E13 Modules | No personalised recommendations module. | New mod_adhritlmsrecommended (prefs → enrolled categories; guest picks; hide-enrolled; top-up) | DONE |
| I-151 | 4.0.0 | E12 API | No REST API for mobile. | Read-only catalog JSON:API + Web Services plugin route registration | DONE |
| I-152 | 4.0.0 | E11 Frontend | Lesson curriculum sidebar stacked above content on phones. | Off-canvas slide-in drawer (site) + admin sidebar drawer (<900px) | DONE |
| I-160 | 3.7.0 | E10 Backup | No full-site backup/restore. | Backup view: full JSON export + restore modes (create-new / modify-existing) | DONE |
| I-161 | 3.7.0 | E10 Backup | No bulk import for lessons/categories. | CSV importers + templates (lessons, categories) | DONE |
| I-162 | 3.7.0 | E5/E3 | trim(null) deprecation on question/lesson/quiz-result edit forms from orphan chapters. | NULL-safe CONCAT_WS chapter labels in the sql fields | DONE |
| I-163 | 3.7.0 | E3 Content | Deleting a course/chapter/lesson left orphan children polluting dropdowns/maps. | Cascade delete: course→children; chapter→detach to General; lesson→detach quizzes | DONE |
| I-170 | 3.6.1 | E2/E3 | Lists looked empty after delete/re-import (stale session filter). | validateFilterId() resets remembered filters pointing at deleted records (5 list models) | DONE |
| I-180 | 3.6.0 | E5 Assessments | Quiz Result edit crashed ('Form::loadForm could not load file'); list warned 'Undefined property $enabled'; search/save crashed on missing columns. | Added quizresult.xml; Passed badge instead of publish; search by course/student; Table::check() without title/slug | DONE |
| I-181 | 3.6.0 | E4 Course Editor | Empty editor state was blank. | Content-map overview table with counts + expandable tree | DONE |
| I-182 | 3.6.0 | E3/E10 | No per-course export/import. | Courses list export (JSON/XML) + restore-as-new | DONE |
| I-183 | 3.6.0 | E5 Assessments | Adding quiz questions one by one was slow. | Bulk CSV import with title auto-mapping + skip reporting | DONE |
| I-184 | 3.6.0 | E9 Settings | Security/Profile/Maps were separate tabs. | Consolidated as sections inside General (same field names) | DONE |
| I-190 | 3.5.0 | E2 Navigation | All left-menu items always visible. | nav_show_* visibility switches; current-page group always shows | DONE |
| I-191 | 3.5.0 | E7 Commerce | No dedicated invoice management. | Invoices admin list (publish/unpublish/delete) under Order Management | DONE |
| I-192 | 3.5.0 | E3 Content | Admin could not preview invoice tax split on a course. | Live tax-breakup panel on the course edit form | DONE |
| I-193 | 3.5.0 | E11 Frontend | COM_ADHRITLMS_FIELD_COURSE_LABEL rendered raw on profile/invoice. | Added the key (and siblings) to the SITE language ini | DONE |
| I-194 | 3.5.0 | E11 Frontend | Invoice 'Prev' button dead when opened in a new tab. | Deferred by request; planned link back to profile Invoices tab | IN PROGRESS |
| I-200 | 3.4.0 | E11 Frontend | My Learning was basic. | Tabbed profile hub (Courses/Wishlist/Certificates/Invoices/Downloads/Preferences/Account) + optional avatar | DONE |
| I-201 | 3.4.0 | E7 Commerce | No invoices at all. | Printable invoice system + Settings → Invoices (3 templates, multi-tax, preview) | DONE |
| I-202 | 3.4.0 | E11 Frontend | Attachments/resources not signposted in the sidebar. | Paperclip markers; chapter resources collapsed by default | DONE |
| I-210 | 3.3.0 | E3/E11 | Lesson attachments rendered empty for plain-URL values; no media picker. | Media-picker attachments subform; JSON + legacy comma decode | DONE |
| I-211 | 3.3.0 | E9/E11 | Frontend section headings not renamable. | Appearance: attachments/resources label overrides | DONE |
| I-212 | 3.3.0 | E11 Frontend | Lesson player didn't show the course name. | Course title H3 (linked) above Back-to-Course | DONE |
| I-213 | 3.3.0 | E1/E11 | getItemId() substring match attached wrong Itemid (course→courses). | Exact view matching | DONE |
| I-214 | 3.3.0 | E11 Frontend | No overall progress bar in the player. | Curriculum sidebar progress bar (% + X of Y, template colour) | DONE |
| I-220 | 3.2.0 | E3/E11 | Chapters could not carry downloadable materials. | Chapter Resources subform + gated streaming endpoint (PDF/ZIP, path-safe) | DONE |
| I-221 | 3.2.0 | E5 Assessments | Passed quizzes could be retaken freely. | Per-course quiz_revisit toggle; enforced on page + submit | DONE |
| I-222 | 3.2.0 | E5 Assessments | After a quiz only 'previous lesson' was offered. | Mark lesson complete & next lesson from the results screen | DONE |
| I-223 | 3.2.0 | E2 Navigation | Sidebar could not be shrunk; group state not restored. | Shrink toggle (icon rail) + restore saved open/closed state | DONE |
| I-230 | 3.1.1 | E2 Navigation | Sidebar links double-encoded (&) → everything fell back to dashboard. | Escape URLs exactly once | DONE |
| I-231 | 3.1.1 | E2 Navigation | Sidebar rendered as a white block. | Restyled to the dark theme | DONE |
| I-240 | 3.1.0 | E2 Navigation | Flat admin menu, hard to scan. | Grouped, collapsible sidebar with quick buttons | DONE |
| I-250 | 3.0.2 | E2 Dashboard | Dashboard overflowed; KPIs unfiltered. | Constrained chart; shared Year/Month/Course/Category KPI filter | DONE |
| I-260 | 3.0.1 | E2 Dashboard | No real dashboard; reviews had no admin. | Modular dashboard (cards/chart/donut/KPIs); Reviews admin section | DONE |
| I-270 | 3.0.0 | E1/E13 | Component installed alone; no module. | Package (pkg_adhritlms) + Catalog module; fixed module catid trim(null) notice | DONE |
| I-280 | 2.5.7 | E5 Assessments | Quiz gating could be bypassed via sidebar/direct link. | Server-side enforcement; locked lessons redirect to the gating quiz | DONE |
| I-290 | 2.5.6 | E11 Frontend | After a quiz, Back went to the sales page; no completion indicators. | Back returns to lesson flow; completion marks + legend + progress bar | DONE |
| I-300 | 2.5.5 | E4 Course Editor | Add buttons lost on scroll. | Floating quick-action bar + back-to-top | DONE |
| I-310 | 2.5.4 | E4/E9 | Editor colours not configurable. | Settings → Course Editor colour controls | DONE |
| I-320 | 2.5.3 | E3 Content | Publish state didn't cascade; disabled-chapter lessons leaked. | Course→chapter/lesson cascade; hide lessons under disabled chapters; appearance colours | DONE |
| I-330 | 2.5.2 | E3 Content | No bulk actions or relational filters on lists. | Bulk publish/unpublish/delete; course/chapter/lesson filters + columns | DONE |
| I-340 | 2.5.1 | E11 Frontend | Wishlist missing on some pages; free courses skipped enrolment; chapter descriptions hidden. | Wishlist on course/category pages; free-course login+enrol; chapter descriptions surfaced | DONE |
| I-350 | 2.5.0 | E4 Course Editor | No content reuse or reordering. | Clone existing chapter/lesson/question picker; drag reorder | DONE |
| I-360 | 2.4.4 | E8 Certificates | Certificate not centered in exports. | Explicit centering of heading/body/logo | DONE |
| I-370 | 2.4.3 | E3 Content | Missing batch-action strings; hard-coded $ in Courses list. | Added strings; currency-formatted prices | DONE |
| I-380 | 2.4.2 | E7 Commerce | Order edit crashed (missing forms/order.xml); no manual orders. | Added order.xml + manual order creation; method/reference columns; auto order numbers | DONE |
| I-390 | 2.4.1 | E4/E7 | Course Editor unreliable; no order management. | Real in-place edit forms; order approve/cancel/delete + manual approval | DONE |
| I-400 | 2.4.0 | E4/E11 | No single-screen builder; flat curriculum. | Course Editor; collapsible curriculum accordion | DONE |
| I-410 | 2.3.5 | E11 Frontend | Encoded ampersands broke redirects (verify result didn't load). | Raw ampersands across 22 site-controller URLs | DONE |
| I-420 | 2.3.0-2.3.4 | E8 Certificates | Certificate export clipping; verify-page captcha/rendering issues. | Off-screen capture sandbox; captcha via form field; borderless verify render; inner-frame designs | DONE |
| I-430 | 2.2.x | E8 Certificates | No certificate customization / verification / download. | 4 templates, borders/frames, logo/signature; client-side PNG/PDF; public verification + sharing | DONE |
| I-440 | 2.1.x | E6/E11 | Category cards not clickable; Courses listing fatal ($this->state unset); no wishlist/profile. | Clickable category cards with counts; fixed listing; Wishlist + My Learning profile | DONE |
| I-450 | 2.0.0 | E3 Content | Two-level only (course→lesson). | Chapters: 3-level hierarchy + chapter quizzes + gating | DONE |
| I-460 | 1.0.0 | E1 Platform | Initial modernised release for Joomla 5/6. | Courses, categories, lessons, quizzes, teachers, events, speakers, orders, certificates, coupons, reviews, payments, dashboard | DONE |