Skip to main content
Instructor API and Dashboard Guide
This file documents the instructor-facing routes and the instructor dashboard/reporting endpoints.

Table of Contents

  1. Overview
  2. Base URL and Headers
  3. List Instructors
  4. Get Instructor by ID
  5. Trainings by Instructor
  6. Exams by Instructor
  7. Quizzes by Instructor
  8. Set Instructor Default Revenue
  9. Dashboard Overview
  10. Monthly Sales
  11. Weekly Sales
  12. Combined Weekly and Monthly Sales
  13. Instructor Earnings Route
  14. Sales Details Summary
  15. Sales Details Orders
  16. Export Dashboard PDF
  17. Export Dashboard CSV
  18. Internal Reused Methods
  19. Known Implementation Notes
  20. Background Items API
    • 20.1 List Background Items for a User
    • 20.2 Create Background Item
    • 20.3 Show Background Item
    • 20.4 Update Background Item
    • 20.5 Delete Background Item

1. Overview

This file covers:
  • instructor discovery and profile lookup
  • instructor-owned trainings and exams
  • default revenue percentage management
  • instructor sales dashboard endpoints
  • instructor sales detail endpoints
  • instructor dashboard exports

2. Base URL and Headers

Base API URL:
Suggested Postman variables:
Headers:
For export endpoints, use:
or:

3. List Instructors

Returns paginated instructors for a company.

Example Request

Success Response


4. Get Instructor by ID

Returns one instructor from the authenticated user’s company.

Example Request

Success Response


5. Trainings by Instructor

Returns paginated trainings created by the instructor.

Example Request

Success Response


6. Exams by Instructor

Returns standalone exams created by the instructor.

Example Request

Success Response


7. Quizzes by Instructor

v4 — Implemented. This route now resolves to UsersController::quizzes_by_instructor().
Returns the quizzes created by the instructor — i.e. a Quiz that is linked to a training and/or a chapter (training_id and/or chapter_id is set). This is the complement of Exams by Instructor (§6), which returns the standalone exams (both FKs null). Each quiz is returned with its categories, reviews, image, purchasing orders, and full question/option tree.
Answer data is stripped (hidden only on this listing — the quiz-authoring endpoints still expose it):
  • All question types: the is_correct flag is removed from every option, so the payload never reveals which choice is correct.
  • Drag & Drop (type 4): the correct answer is encoded in which items belong to which option. To obscure it, the per-option item lists (quiz_question_option_items) are removed and all items are flattened into a single shuffled pool exposed on the question as draggable_items (each with id + label, but no quiz_question_option_id back-reference). The options (drop targets) are still returned with their id/label; the player submits the grouping back via the finish-quiz endpoint.

Example Request

Success Response

Returns [] (empty array) when the instructor has no training/chapter-linked quizzes.

Drag & Drop question shape (answer obscured)

For a quiz_question_type_id = 4 (Drag & Drop) question, the options keep only their id/label (drop targets), the per-option item lists are removed, and the draggable items are returned flattened + shuffled under draggable_items:
The draggable_items order is randomized per request, and no item carries a quiz_question_option_id, so the correct grouping cannot be inferred from the payload.

8. Set Instructor Default Revenue

Updates the instructor’s default revenue percentage.

Request JSON

Success Response


9. Dashboard Overview

Returns high-level revenue and sales metrics for the instructor.

Example Request

Success Response


10. Monthly Sales

Returns month-based sales aggregates for one year.

Query Parameters

Example Request

Success Response

Note:
  • this endpoint divides the numeric values by 1000 before returning them

11. Weekly Sales

Returns week-based sales aggregates for one year.

Example Request

Success Response


12. Combined Weekly and Monthly Sales

Returns both monthly and weekly sales data in one payload.

Example Request

Success Response

Note:
  • in this endpoint, total_sales_trainings and total_sales_exams are earnings totals, not counts

13. Instructor Earnings Route

v3 — Implemented. This route now resolves to InstructorDashboardController::totalInstructorEarnings(). It returns a focused earnings summary (the instructor’s share of every paid sale, split by trainings vs exams) computed with the same paid-order / share logic as the Overview endpoint (§9).
Returns the instructor’s total earnings (their revenue share) across all paid sales.

Example Request

Success Response

Notes:
  • total_instructor_earnings = trainings_earnings + exams_earnings (the instructor’s share).
  • total_revenue is the gross sale price before the share split; *_earnings are the instructor’s cut.
  • An instructor with no trainings and no exams returns the same shape with all numeric fields 0.

Error Responses


14. Sales Details Summary

Returns paginated training and exam summary rows for the instructor.

Query Parameters

Example Request

Success Response


15. Sales Details Orders

Returns the paginated order rows for one selected training or one selected exam.

Query Parameters for Training

Example Request for Training

Success Response for Training

Example Request for Exam

Success Response for Exam

Validation Error Example


16. Export Dashboard PDF

Exports the instructor dashboard summary as a PDF file.

Example Request

Success Behavior

  • returns a PDF download
  • file name pattern:

Error Example


17. Export Dashboard CSV

Exports the instructor dashboard summary as a CSV file.

Example Request

Success Behavior

  • returns a CSV download
  • file name pattern:

CSV Columns Note

The Trainings Sales Count and Exams Sales Count columns previously always exported 0 (they read overview keys that the endpoint never returns). They are now populated from overview’s num_orders_trainings / num_orders_exams (the per-type paid-sale counts). This affects the CSV file only — no JSON API response changed.

Error Example


18. Internal Reused Methods

The controller was refactored to remove the large duplicated “loop paid orders → match the sold item → compute the instructor share” blocks. The shared logic now lives in private helpers suffixed _helper (the public endpoints and all of their JSON responses are unchanged):
  • paid_order_statuses_helper() Brief role: returns the paid order-status IDs (PAID_ONLINE_ID, PAID_OFFLINE_ID) treated as a sale.
  • process_sales_items_helper($items, $instructor, $statuses, $relation, $includeImage) Brief role: aggregates training or exam paid-order revenue for overview() (replaces the old processTrainings()/processExams() twins). $relation is trainings or quizzes; $includeImage adds the model image to the per-item stats row (trainings only).
  • get_top_selling_helper(array $stats, $limit = 3) Brief role: extracts the top-N (default 3) entries by total_revenue for the overview payload (replaces getTopSellingTrainings()/getTopSellingExams()).
  • collect_sale_events_helper($items, $instructor, $statuses, $relation, $year) Brief role: flattens trainings/exams into per-sale events (month, week, pivot_price, discounted_price, pct) consumed by monthlySales(), weeklySales() and weeklyMonthlyCombinedSales(). Each caller keeps its own accumulation rules (see notes in §19).
  • empty_period_buckets_helper($count) Brief role: builds zeroed month/week buckets (1..$count) for both the empty-data branches and as the accumulation base.
  • scale_buckets_helper(array $buckets) Brief role: divides bucket figures by 1000 and formats to 3 decimals (used by monthlySales()/weeklyMonthlyCombinedSales(); not by weeklySales()).
  • summarize_instructor_items_helper($items, $instructor, $statuses, $relation, $type, $includeImage) Brief role: builds the per-training/exam summary rows + aggregate count/income for instructorSalesDetails().
  • build_sale_row_helper($order, $revPct) Brief role: builds a single per-order sale row for instructorSalesDetailsOrders().
  • getThemeForCompany($companyId) Brief role: loads company branding used by exportStatsPdf().
The export methods also reuse other endpoint methods internally:
  • exportStatsPdf() reuses overview(), monthlySales(), and weeklySales()
  • exportStatsCsv() reuses overview()

19. Known Implementation Notes

  • /api/instructors/{instructor_id}/quizzes is implemented as of v4 (UsersController::quizzes_by_instructor()); see §7.
  • /api/instructor/{id}/earnings is implemented as of v3 (totalInstructorEarnings()); see §13.
  • monthlySales() and weeklyMonthlyCombinedSales() divide returned month/week figures by 1000, while weeklySales() returns raw values.
  • weeklySales() uses different underlying user relations than the other dashboard methods, so totals may not perfectly match overview() or monthlySales() in every dataset.
  • The total_trainings_sales / total_exams_sales bucket lines are intentionally not the same quantity across the time-series endpoints: monthlySales() accumulates the (discounted) price, weeklySales() accumulates the pivot price, and weeklyMonthlyCombinedSales() accumulates the instructor share. This pre-existing behaviour is preserved.
  • June 2026 refactor: the controller’s duplicated per-order loops were extracted into _helper methods and a ~170-line commented-out legacy instructorSalesDetails() block was removed. Endpoint signatures, routes and JSON responses are unchanged.

20. Background Items API

Background items represent education, work experience, and certifications attached to a user profile. They are typically used to enrich the instructor’s public profile (resume-like cards), but the routes themselves accept any userId. The model is App\Models\UserBackgroundItem and the controller is App\Http\Controllers\UserBackgroundItemsController.
v3 — Logo storage moved to Cloudflare R2. As of v3, an uploaded logo is stored on Cloudflare R2 (not local server disk), grouped under the files/instructor_background_items/ prefix in the bucket (files/instructor_bg_items/ since v4, below). The created File row is type = 3 (R2) and its url is a public R2 URL. Each logo is saved under a unique object key ({userId}_{time}_{uniqid}_{originalName}), so every File row maps 1-to-1 to its own object — the cross-item deletion bug described in earlier versions is resolved (see §20.5). No frontend change is required: the response shape is unchanged and the logo is still read from image.url; only the URL host changes (now an R2 URL instead of {APP_URL}/storage/...).
v4 — Logo folder and key (01/11/2026). New logos are written to files/instructor_bg_items/{userId}_{time}_{originalName} (the uniqid part is gone; the user id and the second keep the key unique), the folder of the bucket layout on R2 Storage Paths (app/Services/R2StoragePathService.php). Logos uploaded before keep their files/instructor_background_items/… keys and URLs. The response shape is unchanged.
v3.1 — Upload now verified; logo cleanup moved to the model observer. Two reliability fixes:
  1. Verified upload (no more dead URLs). The r2 disk does not enable Flysystem’s throw option, so a failed putFileAs() previously returned false silently while the code still saved a File row — leaving a stored url that returned 404 because the object never reached the bucket. storeLogoFile() now checks the upload result and confirms the object exists in R2 before creating the File row; on failure it throws, so the endpoint returns 500 instead of persisting a broken link.
  2. Disk-correct, guarded cleanup. Logo deletion (on replace and on delete) is now owned by UserBackgroundItemObserver, which removes the object from the correct disk (R2 for type = 3, local public/files/... for legacy type = 1) and only when no other item still references the same logo_id. The previous observer deleted via the default (local) disk, so R2 objects were orphaned.

Model Fields


20.1 List Background Items for a User

Returns all background items for one user, optionally filtered by status.

Query Parameters

Example Request

Success Response


20.2 Create Background Item

Creates a new background item for the given user. Accepts an optional logo file upload. When a logo is sent, it is uploaded to Cloudflare R2 under files/instructor_bg_items/, a File row (type = 3) is created, and its id is stored in logo_id.
The R2 upload is verified before the File row is saved (see §20 v3.1). If the upload to R2 fails, the endpoint returns 500 with the error message and no background item or File row is created — you will never get back a logo URL that 404s.

Request Body (multipart/form-data)

Example Request (JSON-only, no file)

Example Request (with logo upload)

Success Response

Validation Error Example


20.3 Show Background Item

Returns one background item by id.

Example Request

Success Response

Not Found Response


20.4 Update Background Item

Updates one background item. All fields are optional. If logo is provided, it is uploaded to Cloudflare R2 (under files/instructor_bg_items/), a new File row (type = 3) is created, and logo_id is repointed to it. The new upload is verified the same way as create (§20.2) — a failed R2 write returns 500 and does not repoint logo_id.
Note (changed in v3.1): replacing the logo now deletes the previously attached logo — UserBackgroundItemObserver@updating removes the old File row and its R2 object — unless another background item still references that same logo_id (shared/legacy logos are preserved). Earlier versions left the old File/object orphaned.

Request Body (multipart/form-data or JSON)

Example Request

Success Response

Not Found Response


20.5 Delete Background Item

Soft-deletes the background item (inside a DB transaction). If the item has a logo_id, the linked File row is also removed and its object is deleted from the correct disk (R2 for type = 3, local for legacy type = 1) — but only when no other background item still references the same logo_id (guarding shared/legacy logos).
As of v3.1 this cleanup lives in UserBackgroundItemObserver@deleting (a single source of truth fired by $item->delete()), not inline in destroy(). The controller now only soft-deletes the item; the observer handles disk selection and the shared-logo guard.

Example Request

Success Response

Error Response

Resolved Issue (v3)

Earlier versions had a bug: deleting a background item with a logo could destroy the image belonging to another item, because logos were saved on local disk under their raw client filename — two logos sharing a filename overwrote the same physical file, while the File model’s delete hook removed files by filename. This is fixed in v3:
  • Logos are uploaded to R2 under a unique object key ({userId}_{time}_{uniqid}_{originalName}), so every File row maps 1-to-1 to its own object — uploads can no longer collide.
  • The logo File/R2 object is deleted only when no other item references the same logo_id, protecting any pre-existing shared/legacy data.

Additional fixes (v3.1)

  • Dead-URL 404s eliminated. A failed R2 upload used to fail silently (the r2 disk has no throw) yet still saved a File row, producing a stored url that 404’d. Uploads are now verified (storeLogoFile() checks the put result and object existence) before any File row is created.
  • Orphaned R2 objects on delete/replace eliminated. Logo cleanup was deleting from the default (local) disk, so R2 objects were never removed. Cleanup is now centralized in UserBackgroundItemObserver and routes to the correct disk by File::type, with the shared-logo guard applied on both replace (updating) and delete (deleting).
Legacy data: logos uploaded before v3 remain on local disk as type = 1 files with their original URLs; they are not auto-migrated to R2. New and updated logos go to R2. If you need the old logos relocated, run a one-off migration. Already-broken File rows: any File row created before v3.1 whose R2 object never uploaded will still hold a 404 URL — those are not auto-cleaned. Re-upload the logo (or remove the orphan File row) to fix it.