Instructor API and Dashboard Guide
This file documents the instructor-facing routes and the instructor dashboard/reporting endpoints.Table of Contents
- Overview
- Base URL and Headers
- List Instructors
- Get Instructor by ID
- Trainings by Instructor
- Exams by Instructor
- Quizzes by Instructor
- Set Instructor Default Revenue
- Dashboard Overview
- Monthly Sales
- Weekly Sales
- Combined Weekly and Monthly Sales
- Instructor Earnings Route
- Sales Details Summary
- Sales Details Orders
- Export Dashboard PDF
- Export Dashboard CSV
- Internal Reused Methods
- Known Implementation Notes
- 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: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_correctflag 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 asdraggable_items(each withid+label, but noquiz_question_option_idback-reference). The options (drop targets) are still returned with theirid/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 aquiz_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:
Thedraggable_itemsorder is randomized per request, and no item carries aquiz_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
- this endpoint divides the numeric values by
1000before 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
- in this endpoint,
total_sales_trainingsandtotal_sales_examsare 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
total_instructor_earnings=trainings_earnings+exams_earnings(the instructor’s share).total_revenueis the gross sale price before the share split;*_earningsare 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
TheTrainings 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 foroverview()(replaces the oldprocessTrainings()/processExams()twins).$relationistrainingsorquizzes;$includeImageadds the modelimageto the per-item stats row (trainings only).get_top_selling_helper(array $stats, $limit = 3)Brief role: extracts the top-N (default 3) entries bytotal_revenuefor the overview payload (replacesgetTopSellingTrainings()/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 bymonthlySales(),weeklySales()andweeklyMonthlyCombinedSales(). 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 by1000and formats to 3 decimals (used bymonthlySales()/weeklyMonthlyCombinedSales(); not byweeklySales()).summarize_instructor_items_helper($items, $instructor, $statuses, $relation, $type, $includeImage)Brief role: builds the per-training/exam summary rows + aggregate count/income forinstructorSalesDetails().build_sale_row_helper($order, $revPct)Brief role: builds a single per-order sale row forinstructorSalesDetailsOrders().getThemeForCompany($companyId)Brief role: loads company branding used byexportStatsPdf().
exportStatsPdf()reusesoverview(),monthlySales(), andweeklySales()exportStatsCsv()reusesoverview()
19. Known Implementation Notes
/api/instructors/{instructor_id}/quizzesis implemented as of v4 (UsersController::quizzes_by_instructor()); see §7./api/instructor/{id}/earningsis implemented as of v3 (totalInstructorEarnings()); see §13.monthlySales()andweeklyMonthlyCombinedSales()divide returned month/week figures by1000, whileweeklySales()returns raw values.weeklySales()uses different underlying user relations than the other dashboard methods, so totals may not perfectly matchoverview()ormonthlySales()in every dataset.- The
total_trainings_sales/total_exams_salesbucket lines are intentionally not the same quantity across the time-series endpoints:monthlySales()accumulates the (discounted) price,weeklySales()accumulates the pivot price, andweeklyMonthlyCombinedSales()accumulates the instructor share. This pre-existing behaviour is preserved. - June 2026 refactor: the controller’s duplicated per-order loops were extracted into
_helpermethods and a ~170-line commented-out legacyinstructorSalesDetails()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 anyuserId.
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 uploadedlogois stored on Cloudflare R2 (not local server disk), grouped under thefiles/instructor_background_items/prefix in the bucket (files/instructor_bg_items/since v4, below). The createdFilerow istype = 3(R2) and itsurlis a public R2 URL. Each logo is saved under a unique object key ({userId}_{time}_{uniqid}_{originalName}), so everyFilerow 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 fromimage.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 tofiles/instructor_bg_items/{userId}_{time}_{originalName}(theuniqidpart 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 theirfiles/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:
- Verified upload (no more dead URLs). The
r2disk does not enable Flysystem’sthrowoption, so a failedputFileAs()previously returnedfalsesilently while the code still saved aFilerow — leaving a storedurlthat returned 404 because the object never reached the bucket.storeLogoFile()now checks the upload result and confirms the object exists in R2 before creating theFilerow; on failure it throws, so the endpoint returns500instead of persisting a broken link.- 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 fortype = 3, localpublic/files/...for legacytype = 1) and only when no other item still references the samelogo_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 bystatus.
Query Parameters
Example Request
Success Response
20.2 Create Background Item
Creates a new background item for the given user. Accepts an optionallogo 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 theFilerow is saved (see §20 v3.1). If the upload to R2 fails, the endpoint returns500with the error message and no background item orFilerow is created — you will never get back alogoURL 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. Iflogo 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@updatingremoves the oldFilerow and its R2 object — unless another background item still references that samelogo_id(shared/legacy logos are preserved). Earlier versions left the oldFile/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 alogo_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 inUserBackgroundItemObserver@deleting(a single source of truth fired by$item->delete()), not inline indestroy(). 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 theFile 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 everyFilerow 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 samelogo_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
r2disk has nothrow) yet still saved aFilerow, producing a storedurlthat 404’d. Uploads are now verified (storeLogoFile()checks the put result and object existence) before anyFilerow 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 inUserBackgroundItemObserverand routes to the correct disk byFile::type, with the shared-logo guard applied on both replace (updating) and delete (deleting).
Legacy data: logos uploaded before v3 remain on local disk astype = 1files 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-brokenFilerows: anyFilerow 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 orphanFilerow) to fix it.