1269 lines
49 KiB
Markdown
1269 lines
49 KiB
Markdown
# Backend API — response model gaps
|
|
|
|
This document tracks fields the frontend already consumes that are **not yet
|
|
present** in the current backend response models. Each section lists:
|
|
|
|
- **Postman / current spec**: what the backend returns today.
|
|
- **UI also needs**: fields the frontend mock fixture exposes and the UI reads
|
|
from. These must be added to the real response to remove the mock.
|
|
- **Used by**: where on the frontend the field is consumed.
|
|
|
|
Naming convention: the backend uses `snake_case`; the HTTP client camelizes
|
|
responses, so this doc lists backend keys in `snake_case` and notes the
|
|
camelCase form the UI sees.
|
|
|
|
---
|
|
|
|
## ⚠️ Cross-cutting: `name` vs `first_name` / `last_name`
|
|
|
|
The current Postman models expose only a single `name` field on the user
|
|
object. **The UI cannot work with `name` alone.**
|
|
|
|
- The signup form (`/register`) collects **two separate inputs**:
|
|
*نام* (first name) and *نام خانوادگی* (last name). If the backend
|
|
only stores `name`, these two inputs collapse on the server and
|
|
cannot be returned to the UI later.
|
|
- After signup, `GET /auth/me` is the source of truth for the
|
|
authenticated user — the layout header, the dashboard, and
|
|
`EditProfilePage.vue` all read `firstName` / `lastName` (not
|
|
`name`). A `/auth/me` response without `first_name` / `last_name`
|
|
leaves these views blank.
|
|
- Every other user-returning endpoint has the same need
|
|
(`/admin/users` list rows, `/admin/users/:id` detail modal,
|
|
`/admin/users` create, `/admin/users/:id` update, `/admin/users/:id/role`
|
|
assign-roles response, login response `data.user`, register response,
|
|
etc.).
|
|
|
|
**Required action on the backend side:**
|
|
|
|
1. Persist `first_name` and `last_name` as **independent columns** on
|
|
the user table — accept them as separate keys in every write
|
|
endpoint that touches the user (`/register`, `POST /admin/users`,
|
|
`PATCH /admin/users/:id`).
|
|
2. Return `first_name`, `last_name`, **and** the convenience
|
|
`full_name` (or keep `name`) in **every** user-returning response.
|
|
The UI will continue reading `firstName` / `lastName`; `fullName` /
|
|
`name` are used only where a single display string is needed
|
|
(tables, badges).
|
|
3. If the existing data already only has `name`, do a one-time
|
|
migration (split on the first whitespace, fall back to leaving
|
|
`last_name` empty for single-token names). New writes must keep
|
|
the two columns in sync with `name` / `full_name`.
|
|
|
|
The per-endpoint tables below repeat `first_name` / `last_name` under
|
|
each "Missing — UI also needs" section so the backend dev cannot miss
|
|
it on individual routes; this top section explains *why* it is the
|
|
same answer everywhere.
|
|
|
|
---
|
|
|
|
## POST `/media/upload` — single-step media upload (canonical)
|
|
|
|
**This is the only media-upload endpoint in the project.** Earlier
|
|
revisions of this doc referenced per-resource cover endpoints
|
|
(`POST /terms/:id/image`, `POST /courses/:id/image`,
|
|
`POST /sessions/:id/media`) — those were a mistake and have been
|
|
removed from the FE, the API client, and the mock.
|
|
|
|
### Flow
|
|
|
|
1. User picks a file (avatar / cover / attachment) in the form.
|
|
2. FE posts the file as `multipart/form-data` to `POST /media/upload`
|
|
(was `POST /upload-temp`).
|
|
3. Backend stores the file and returns its media `id`.
|
|
4. FE puts that id on the resource create/update body under whichever
|
|
key the resource expects: `avatar_id`, `image_id`, `cover_id`,
|
|
`material_id`, etc.
|
|
5. The resource endpoint (`POST /courses`, `PATCH /terms/:id`,
|
|
`POST /admin/users`, …) resolves the id and attaches the media.
|
|
|
|
The previous mock endpoint path was `/upload-temp`. It now lives at
|
|
`/media/upload`. The FE function `useUploadTemporaryMutation` was
|
|
renamed to `useUploadMediaMutation` (consumed by the profile form,
|
|
admin-user form, admin-term form, admin-course forms, admin-session
|
|
form, and the student-register media/personal-info components).
|
|
|
|
### Mock request
|
|
|
|
`multipart/form-data` with a single `file` field.
|
|
|
|
### Mock response
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Media uploaded.",
|
|
"data": {
|
|
"id": 1234,
|
|
"upload_id": 1234,
|
|
"url": "https://…/240/240"
|
|
}
|
|
}
|
|
```
|
|
|
|
`upload_id` is an alias for `id` and is what older FE code reads
|
|
(`form.value.imageId = payload?.uploadId || payload?.id`). Backend
|
|
should return both keys, or eventually the FE migrates to reading
|
|
only `id`.
|
|
|
|
### How the resource body references the upload
|
|
|
|
| Resource form | Field on the body | Backend should treat it as a media id |
|
|
| -------------------- | ------------------ | -------------------------------------- |
|
|
| `/auth/me` profile | `avatar_id` | yes |
|
|
| `POST /admin/users` | `avatar_id` | yes |
|
|
| `POST /terms` | `image_id` | yes — sets `cover_url` |
|
|
| `POST /courses` | `image_id` | yes — sets `cover_url` |
|
|
| `POST /sessions` | `image_id` / `materials[]` | yes — `image_id` sets cover; `materials` array carries session attachments by their media ids |
|
|
|
|
> **Decision pending:** rename FE field `image_id` → `media_id`
|
|
> across the board, or have the backend accept the resource-specific
|
|
> field names. The mock currently honors the resource-specific names.
|
|
|
|
---
|
|
|
|
## GET `/auth/me` — current user
|
|
|
|
Mock fixture: `src/services/mock/fixtures/me.js` (`currentMe`).
|
|
Frontend query: `useGetMeQuery` in `src/services/query/auth.js`.
|
|
|
|
> This endpoint is **the source of truth for the logged-in user**
|
|
> immediately after `/register` (signup). Anything the signup form
|
|
> collects but `/auth/me` doesn't return is effectively lost from the
|
|
> UI's perspective. See the cross-cutting `name` vs
|
|
> `first_name`/`last_name` note above before reviewing this section.
|
|
|
|
### Spec (already implemented)
|
|
|
|
| Backend key | UI key (camelCase) | Type | Notes |
|
|
| ---------------------- | --------------------- | -------- | -------------------------------------- |
|
|
| `id` | `id` | number | |
|
|
| `name` | `name` | string | Full display name |
|
|
| `email` | `email` | string | |
|
|
| `phone` | `phone` | string | E.164, e.g. `+989121234567` |
|
|
| `roles` | `roles` | string[] | e.g. `["student"]` |
|
|
| `avatar_url` | `avatarUrl` | string | Public avatar URL |
|
|
| `avatar_download_url` | `avatarDownloadUrl` | string | Signed/download endpoint |
|
|
| `created_at` | `createdAt` | string | ISO 8601 |
|
|
|
|
### Missing — UI also needs
|
|
|
|
| Backend key (proposed) | UI key (camelCase) | Type | Used by |
|
|
| ------------------------- | --------------------- | -------- | ------------------------------------------------------------------------- |
|
|
| `first_name` | `firstName` | string | `EditProfilePage.vue` form, header/greeting |
|
|
| `last_name` | `lastName` | string | `EditProfilePage.vue` form |
|
|
| `full_name` | `fullName` | string | Lists/tables that render a single name string |
|
|
| `phone_number` | `phoneNumber` | string | `EditProfilePage.vue` form (local format, e.g. `09121234567`) |
|
|
| `national_code` | `nationalCode` | string | `EditProfilePage.vue` form, identity validation |
|
|
| `status` | `status` | string | enum: `pending` \| `approved` \| `rejected` — gates access in the UI |
|
|
| `address.address` | `address.address` | string | `EditProfilePage.vue` address textarea |
|
|
| `address.province.id` | `address.province.id` | number | `EditProfilePage.vue` province select |
|
|
| `address.province.name` | `address.province.name` | string | |
|
|
| `address.city.id` | `address.city.id` | number | `EditProfilePage.vue` city select |
|
|
| `address.city.name` | `address.city.name` | string | |
|
|
| `profile.bio` | `profile.bio` | string | `EditProfilePage.vue` bio textarea |
|
|
| `profile.birth_date` | `profile.birthDate` | string | ISO 8601, `EditProfilePage.vue` date picker |
|
|
| `profile.marital_status` | `profile.maritalStatus` | string | enum: `single` \| `married` |
|
|
| `profile.gender` | `profile.gender` | string | enum: `male` \| `female` |
|
|
| `profile.education_status`| `profile.educationStatus` | string | enum |
|
|
| `profile.seminary_level` | `profile.seminaryLevel` | string | enum |
|
|
| `profile.university_level`| `profile.universityLevel` | string | enum |
|
|
| `profile.university_name` | `profile.universityName` | string | |
|
|
| `profile.field_of_study` | `profile.fieldOfStudy` | string | |
|
|
| `profile.avatar_id` | `profile.avatarId` | number \| null | Media id returned by `POST /media/upload` |
|
|
|
|
### Suggested response shape
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "OK",
|
|
"data": {
|
|
"id": 5,
|
|
"name": "Jane Student",
|
|
"first_name": "Jane",
|
|
"last_name": "Student",
|
|
"full_name": "Jane Student",
|
|
"email": "jane.student@example.com",
|
|
"phone": "+989121234567",
|
|
"phone_number": "09121234567",
|
|
"national_code": "0079827498",
|
|
"status": "approved",
|
|
"roles": ["student"],
|
|
"avatar_url": "http://localhost:8080/storage/users/5/avatar.jpg",
|
|
"avatar_download_url": "http://localhost:8080/api/media/9/download",
|
|
"created_at": "2026-02-01T10:00:00+00:00",
|
|
"address": {
|
|
"address": "...",
|
|
"province": { "id": 1, "name": "تهران" },
|
|
"city": { "id": 11, "name": "تهران" }
|
|
},
|
|
"profile": {
|
|
"bio": "...",
|
|
"birth_date": "1989-06-12T00:00:00.000Z",
|
|
"marital_status": "married",
|
|
"gender": "male",
|
|
"education_status": "graduate",
|
|
"seminary_level": "level_3",
|
|
"university_level": "master",
|
|
"university_name": "تهران",
|
|
"field_of_study": "علوم قرآنی",
|
|
"avatar_id": null
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## GET `/admin/users` — list users (paginated)
|
|
|
|
Mock fixture: `src/services/mock/fixtures/admin-users.js` (`adminUsers`, `makeUser`).
|
|
Mock route: `src/services/mock/routes/admin-users.js`.
|
|
Frontend query: `useAdminUsersListQuery` in `src/services/query/admin-users.js`.
|
|
Consumed by: `src/features/admin/users/pages/UsersListPage.vue`,
|
|
`src/features/admin/users/components/UsersTable.vue`.
|
|
|
|
### Spec envelope
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "OK",
|
|
"data": {
|
|
"items": [ /* user objects */ ],
|
|
"meta": {
|
|
"current_page": 1,
|
|
"per_page": 20,
|
|
"total": 3,
|
|
"last_page": 1
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Query params the UI already sends: `search`, `per_page`, `page`, `name`,
|
|
`national_code`, `phone_number`, `role_id`, `from_date`, `to_date`. The
|
|
spec only documents `search` + `per_page` — backend should accept the rest
|
|
or the UI filters need to be reworked.
|
|
|
|
### Per-item spec (already implemented)
|
|
|
|
| Backend key | UI key (camelCase) | Type |
|
|
| ---------------------- | --------------------- | -------- |
|
|
| `id` | `id` | number |
|
|
| `name` | `name` | string |
|
|
| `email` | `email` | string |
|
|
| `phone` | `phone` | string \| null |
|
|
| `roles` | `roles` | string[] |
|
|
| `avatar_url` | `avatarUrl` | string \| null |
|
|
| `avatar_download_url` | `avatarDownloadUrl` | string \| null |
|
|
| `created_at` | `createdAt` | string |
|
|
|
|
### Per-item missing — UI also needs
|
|
|
|
| Backend key (proposed) | UI key (camelCase) | Used by |
|
|
| ------------------------- | --------------------- | ------------------------------------------------------------------------ |
|
|
| `first_name` | `firstName` | `UsersTable.vue` (display name), filters |
|
|
| `last_name` | `lastName` | `UsersTable.vue`, delete-confirm dialog |
|
|
| `phone_number` | `phoneNumber` | `UsersTable.vue` (local format display), `UserDetailsModal.vue` |
|
|
| `national_code` | `nationalCode` | `UsersTable.vue`, `UserDetailsModal.vue` |
|
|
| `status` | `status` | enum `pending` \| `approved` \| `blocked` — `UserDetailsModal.vue` badge |
|
|
| `role_id` | `roleId` | `UsersTable.vue` role-change dropdown |
|
|
|
|
---
|
|
|
|
## GET `/admin/users/:id` — show user
|
|
|
|
Mock route: same file as above.
|
|
Frontend query: `useAdminUserQuery` in `src/services/query/admin-users.js`.
|
|
Consumed by: `src/features/admin/users/components/modals/UserDetailsModal.vue`
|
|
(opened from the users list — *not* a route page).
|
|
|
|
### Spec envelope
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "OK",
|
|
"data": { /* user object */ }
|
|
}
|
|
```
|
|
|
|
### Spec fields (already implemented)
|
|
|
|
Same as list-item spec above.
|
|
|
|
### Missing — UI also needs
|
|
|
|
Detail modal renders the full profile, so it needs the list-item missing
|
|
fields **plus** the nested `address` and `profile` blocks:
|
|
|
|
| Backend key (proposed) | UI key (camelCase) | Type |
|
|
| ------------------------- | ------------------------- | ---------------- |
|
|
| `address.address` | `address.address` | string |
|
|
| `address.province.id` | `address.province.id` | number |
|
|
| `address.province.name` | `address.province.name` | string |
|
|
| `address.city.id` | `address.city.id` | number |
|
|
| `address.city.name` | `address.city.name` | string |
|
|
| `bio` | `bio` | string |
|
|
| `birth_date` | `birthDate` | string (ISO) |
|
|
| `fa_birth_date` | `faBirthDate` | string (Jalaali) |
|
|
| `marital_status` | `maritalStatus` | enum |
|
|
| `gender` | `gender` | enum |
|
|
| `education_status` | `educationStatus` | enum |
|
|
| `seminary_level` | `seminaryLevel` | enum |
|
|
| `university_level` | `universityLevel` | enum |
|
|
| `university_name` | `universityName` | string |
|
|
| `field_of_study` | `fieldOfStudy` | string |
|
|
| `profile_completed` | `profileCompleted` | boolean |
|
|
|
|
> The modal flattens these (`user.bio`, `user.birthDate`, …) rather than
|
|
> reading from `user.profile.*`, so backend can either return them flat
|
|
> at the top level or the FE select can flatten them — whichever is
|
|
> easier. Document this once decided.
|
|
|
|
### 404 — Not found
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"message": "Resource not found."
|
|
}
|
|
```
|
|
|
|
The FE `findOrThrow` mock throws a 404; the toast/error layer surfaces
|
|
the `message` field — keep that key name.
|
|
|
|
---
|
|
|
|
## POST `/admin/users` — create user
|
|
|
|
Mock route: `src/services/mock/routes/admin-users.js`.
|
|
API client: `apiAddAdminUser` in `src/services/api/admin-users.js`.
|
|
Query: `useAddAdminUserMutation` in `src/services/query/admin-users.js`.
|
|
Consumed by: `src/features/admin/users/pages/UserFormPage.vue` (add mode).
|
|
|
|
### Spec request body
|
|
|
|
```json
|
|
{
|
|
"name": "New Teacher",
|
|
"email": "teacher.new@example.com",
|
|
"phone": "+989120000000",
|
|
"password": "StrongPass123!",
|
|
"roles": ["teacher"]
|
|
}
|
|
```
|
|
|
|
### What the UI form actually sends today
|
|
|
|
`UserFormPage.vue` posts a much richer payload (snake-cased by the HTTP
|
|
interceptor before going out). The keys that map to the spec are marked
|
|
with `→ spec`; the rest have no spec equivalent yet:
|
|
|
|
| FE form key (camelCase) | Wire key (snake_case) | Maps to spec? |
|
|
| ------------------------- | ---------------------------- | ------------- |
|
|
| `firstName` | `first_name` | → derives `name` together with `lastName` |
|
|
| `lastName` | `last_name` | → derives `name` |
|
|
| `phoneNumber` | `phone_number` | → maps to `phone` (needs `09…` → `+98…`) |
|
|
| `roleId` | `role_id` | → maps to `roles` (single id → name array) |
|
|
| `password` | `password` | → `password` |
|
|
| `passwordConfirmation` | `password_confirmation` | (spec doesn't document, FE still sends) |
|
|
| `nationalCode` | `national_code` | missing |
|
|
| `birthDate` | `birth_date` | missing |
|
|
| `maritalStatus` | `marital_status` | missing |
|
|
| `gender` | `gender` | missing |
|
|
| `bio` | `bio` | missing |
|
|
| `provinceId` | `province_id` | missing |
|
|
| `cityId` | `city_id` | missing |
|
|
| `address` | `address` | missing |
|
|
| `avatarId` | `avatar_id` | missing (returned by `POST /media/upload`) |
|
|
|
|
> **The form has no `email` input.** The spec requires `email`. Either
|
|
> the backend should treat `email` as optional (auto-generate or accept
|
|
> null), or the FE form needs a new field. Decision pending.
|
|
|
|
### Spec response — `201 Created`
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "User created.",
|
|
"data": { /* user object, same shape as GET /admin/users/:id */ }
|
|
}
|
|
```
|
|
|
|
The response should carry every field listed for `GET /admin/users/:id`
|
|
(spec + "ui also needs" — list-item missing fields and the nested
|
|
`address`/`profile` blocks), so the UI can refresh the cache without an
|
|
extra round-trip.
|
|
|
|
### Spec response — `422 Unprocessable Entity`
|
|
|
|
```json
|
|
{
|
|
"message": "The given data was invalid.",
|
|
"errors": {
|
|
"email": ["The email field is required."]
|
|
}
|
|
}
|
|
```
|
|
|
|
The FE doesn't currently render per-field server errors — it would need
|
|
a small hook into the form's `errors` ref. Out of scope for the
|
|
backend, but worth flagging.
|
|
|
|
---
|
|
|
|
## PATCH `/admin/users/:id` — update user
|
|
|
|
Mock route: same file.
|
|
API client: `apiUpdateAdminUser` — **changed from `PUT` to `PATCH`** to
|
|
match the spec.
|
|
Query: `useUpdateAdminUserMutation`.
|
|
Consumed by: `src/features/admin/users/pages/UserFormPage.vue` (edit mode).
|
|
|
|
### Spec request body
|
|
|
|
```json
|
|
{
|
|
"name": "Renamed User",
|
|
"roles": ["teacher", "counselor"]
|
|
}
|
|
```
|
|
|
|
Spec implies partial update — only the keys present are changed.
|
|
|
|
### What the UI form actually sends today
|
|
|
|
Same rich payload as `POST` above (the same form is used in add + edit
|
|
mode). Notable differences:
|
|
|
|
- No `password` / `password_confirmation` in edit mode.
|
|
- `national_code` is sent but is disabled in the UI when the user
|
|
already has one.
|
|
- The FE still sends only a single role via `role_id`, not an array
|
|
via `roles`. Backend must either accept `role_id` *or* the FE must
|
|
switch to sending `roles: [name]`. Decision pending.
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "User updated.",
|
|
"data": { /* user object, same shape as GET /admin/users/:id */ }
|
|
}
|
|
```
|
|
|
|
Same superset of fields as `POST` response — see `GET /admin/users/:id`
|
|
table for the full list the UI needs back.
|
|
|
|
---
|
|
|
|
## DELETE `/admin/users/:id` — delete user
|
|
|
|
Mock route: `src/services/mock/routes/admin-users.js`.
|
|
API client: `apiDeleteAdminUser` in `src/services/api/admin-users.js`.
|
|
Query: `useDeleteAdminUserMutation` in `src/services/query/admin-users.js`.
|
|
Consumed by: `src/features/admin/users/pages/UsersListPage.vue`
|
|
(via `ConfirmModal` → `onConfirm`).
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "User deleted.",
|
|
"data": null
|
|
}
|
|
```
|
|
|
|
The FE only checks for a non-error completion (the mutation's
|
|
`onSuccess` invalidates the list cache); the response body isn't read.
|
|
No additional fields needed.
|
|
|
|
> The previous mock returned a Persian success message
|
|
> (`حذف موفق`) under a `data.message` key. That has been replaced by the
|
|
> spec envelope above. If a localized toast is needed, the FE composes
|
|
> it client-side rather than reading `data.message`.
|
|
|
|
---
|
|
|
|
## PATCH `/admin/users/:id/role` — assign roles
|
|
|
|
Mock route: `src/services/mock/routes/admin-users.js`.
|
|
API client: `apiUpdateAdminUserRole` — **changed from `POST` to `PATCH`**
|
|
to match the spec.
|
|
Query: `useUpdateAdminUserRoleMutation`.
|
|
Consumed by: `src/features/admin/users/components/UsersTable.vue`
|
|
(role-badge dropdown → `onChangeRole`).
|
|
|
|
### Spec request body
|
|
|
|
```json
|
|
{
|
|
"roles": ["missionary"]
|
|
}
|
|
```
|
|
|
|
The FE now sends `{ roles: [targetRoleName] }` exactly per spec.
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Roles updated.",
|
|
"data": { /* user object, same shape as GET /admin/users/:id */ }
|
|
}
|
|
```
|
|
|
|
The mock additionally writes `roleId` (legacy UI key) onto the user so
|
|
the same record stays consistent in the in-memory store. The FE
|
|
mutation only triggers `invalidateUsers` on success, so the envelope
|
|
itself is not read.
|
|
|
|
> The mock also accepts legacy bodies (`{ roleId }` and `{ role }`)
|
|
> as a fallback in case other callers exist; new code should send
|
|
> `{ roles: [...] }`.
|
|
|
|
---
|
|
|
|
## Terms (`/terms`) — global notes
|
|
|
|
The previous FE wiring used `/admin/terms` paths; the spec uses `/terms`.
|
|
All five term endpoints below moved to the `/terms` namespace. The
|
|
admin-only auxiliary endpoints (`/admin/terms/:id/clone`,
|
|
`/admin/terms/:id/status`, the term-students sub-tree, the term-courses
|
|
sub-tree) **stayed** on `/admin/terms` until the backend specs them.
|
|
|
|
### ⚠️ Field name mismatches (apply to every terms endpoint below)
|
|
|
|
| FE / mock key (camelCase) | Spec key (snake_case) | Notes |
|
|
| ------------------------- | --------------------- | -------------------------------------- |
|
|
| `image` | `cover_url` | FE reads `term.image` |
|
|
| `startDate` | `starts_at` | FE reads `term.startDate` |
|
|
| `endDate` | `ends_at` | FE reads `term.endDate` |
|
|
| `studentsCount` | *(missing)* | Used in list view |
|
|
| `coursesCount` | *(missing)* | Used in list view |
|
|
| `faStartDate` / `faEndDate` | *(missing)* | Jalaali-formatted display strings; FE can compute client-side, but if backend provides them the FE saves a `formatJalaaliDate` call |
|
|
|
|
The mock currently writes **both** the spec key and the legacy UI key
|
|
(`coverUrl` + `image`, `startsAt` + `startDate`, `endsAt` + `endDate`)
|
|
onto every fixture and response. **Required action:** backend should
|
|
either rename to the legacy keys, ship both, or the FE consumers
|
|
(`TermItem.vue`, `TermFormPage.vue`, `TermsListPage.vue`) need to be
|
|
migrated to read the spec keys. Decision pending.
|
|
|
|
---
|
|
|
|
## GET `/terms` — list terms (paginated)
|
|
|
|
Mock fixture: `src/services/mock/fixtures/admin-terms.js` (`adminTerms`).
|
|
Mock route: `src/services/mock/routes/admin-terms.js`.
|
|
Frontend query: `useAdminTermsListQuery`.
|
|
Consumed by: `src/features/admin/terms/pages/TermsListPage.vue`,
|
|
`src/features/admin/terms/components/TermItem.vue`.
|
|
|
|
### Spec envelope
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "OK",
|
|
"data": {
|
|
"items": [ /* term objects */ ],
|
|
"meta": { "current_page": 1, "per_page": 20, "total": 2, "last_page": 1 }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Per-item spec
|
|
|
|
| Backend key | UI key | Type |
|
|
| -------------- | ----------- | -------------- |
|
|
| `id` | `id` | number |
|
|
| `title` | `title` | string |
|
|
| `description` | `description` | string |
|
|
| `is_active` | `isActive` | boolean |
|
|
| `starts_at` | `startsAt` | string (ISO) |
|
|
| `ends_at` | `endsAt` | string (ISO) |
|
|
| `cover_url` | `coverUrl` | string \| null |
|
|
| `created_at` | `createdAt` | string (ISO) |
|
|
|
|
### Per-item missing — UI also needs
|
|
|
|
See "Field name mismatches" above + `students_count` and
|
|
`courses_count` for the term list cards.
|
|
|
|
Query params the FE sends: `per_page`, `page`, `title` (free-text),
|
|
`status` (0/1), `from_date`/`to_date`. The spec only documents
|
|
`per_page` and `active_only=1` — backend should accept the rest or
|
|
drop them from the FE.
|
|
|
|
### 401 — Unauthenticated
|
|
|
|
```json
|
|
{ "message": "Unauthenticated." }
|
|
```
|
|
|
|
The global HTTP layer already redirects to `/login` on 401 (see
|
|
`addUnauthorizeInterceptor` in `src/services/api/http.js`).
|
|
|
|
---
|
|
|
|
## GET `/terms/:id` — show term
|
|
|
|
Mock route: same file.
|
|
Frontend query: `useAdminTermQuery`.
|
|
Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (edit
|
|
mode), `src/features/admin/terms/components/modals/TermDetailsModal.vue`.
|
|
|
|
### Spec envelope
|
|
|
|
```json
|
|
{ "success": true, "message": "OK", "data": { /* term object */ } }
|
|
```
|
|
|
|
Same fields as a list item. The detail modal needs the same UI-only
|
|
keys (`image`, `startDate`, `endDate`, `studentsCount`, `coursesCount`).
|
|
|
|
### 404 — Not found
|
|
|
|
```json
|
|
{ "success": false, "message": "Resource not found." }
|
|
```
|
|
|
|
---
|
|
|
|
## POST `/terms` — create term
|
|
|
|
Mock route: same file.
|
|
API client: `apiAddAdminTerm`.
|
|
Query: `useAddAdminTermMutation`.
|
|
Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (add).
|
|
|
|
### Spec request body
|
|
|
|
```json
|
|
{
|
|
"title": "Spring 2026",
|
|
"description": "Spring term covering intro & intermediate units.",
|
|
"is_active": true,
|
|
"starts_at": "2026-03-20",
|
|
"ends_at": "2026-06-20"
|
|
}
|
|
```
|
|
|
|
### What the FE form sends today
|
|
|
|
| FE key (camelCase) | Wire key | Maps to spec? |
|
|
| ------------------ | -------------- | -------------------------------------- |
|
|
| `title` | `title` | → `title` |
|
|
| `description` | `description` | → `description` |
|
|
| `startDate` | `start_date` | → `starts_at` (key rename needed) |
|
|
| `endDate` | `end_date` | → `ends_at` (key rename needed) |
|
|
| `imageId` | `image_id` | not in spec body — see "Image flow" below |
|
|
|
|
**Image flow** (canonical): the FE uploads the file to
|
|
`POST /media/upload`, gets a media `id` back, and submits it as
|
|
`image_id` with the term create/update body. The backend should
|
|
resolve `image_id` against the media table and populate
|
|
`cover_url`. No separate per-resource upload endpoint exists —
|
|
the previously documented `POST /terms/:id/image` was a mistake
|
|
and has been removed.
|
|
|
|
`is_active` is required by the spec but the FE form doesn't expose a
|
|
toggle on create (terms are forced to active). The mock defaults it
|
|
to `true`; backend should treat `is_active` as optional with a
|
|
default of `true`, or the FE needs a switch on the create form.
|
|
|
|
### Spec response — `201 Created`
|
|
|
|
```json
|
|
{ "success": true, "message": "Term created.", "data": { /* term */ } }
|
|
```
|
|
|
|
The response should carry every "UI also needs" field from the list
|
|
section so the FE can refresh the cache without an extra GET.
|
|
|
|
### Spec errors
|
|
|
|
- `403` `{ "success": false, "message": "This action is unauthorized." }`
|
|
- `422` `{ "message": "The given data was invalid.", "errors": { … } }`
|
|
|
|
FE does not yet render per-field server errors.
|
|
|
|
---
|
|
|
|
## PATCH `/terms/:id` — update term
|
|
|
|
Mock route: same file.
|
|
API client: `apiUpdateAdminTerm` — **changed from `PUT` to `PATCH`**.
|
|
Query: `useUpdateAdminTermMutation`.
|
|
Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (edit).
|
|
|
|
### Spec request body — partial
|
|
|
|
```json
|
|
{ "title": "Spring 2026 (Revised)", "is_active": true }
|
|
```
|
|
|
|
### What the FE form sends today
|
|
|
|
Same payload as create (the same form handles add + edit). The mock
|
|
applies a true partial update — keys that are `undefined` in the
|
|
payload are left unchanged on the stored term.
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{ "success": true, "message": "Term updated.", "data": { /* term */ } }
|
|
```
|
|
|
|
### Spec errors
|
|
|
|
- `403` `{ "success": false, "message": "This action is unauthorized." }`
|
|
|
|
---
|
|
|
|
## DELETE `/terms/:id` — delete term
|
|
|
|
Mock route: same file.
|
|
API client: `apiDeleteAdminTerm`.
|
|
Query: `useDeleteAdminTermMutation`.
|
|
Consumed by: `src/features/admin/terms/pages/TermsListPage.vue`
|
|
(via `ConfirmModal`).
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{ "success": true, "message": "Term deleted.", "data": null }
|
|
```
|
|
|
|
### Spec errors
|
|
|
|
- `403` `{ "success": false, "message": "This action is unauthorized." }`
|
|
|
|
The previous mock returned a Persian success message under
|
|
`data.message` — replaced by the spec envelope. FE only invalidates
|
|
the list cache on success and never reads the body.
|
|
|
|
---
|
|
|
|
## Courses (`/courses`) — global notes
|
|
|
|
These spec endpoints are for **offered courses** (an instance of a course
|
|
template tied to a term + teacher). The previous FE wiring used
|
|
`/admin/courses`; the spec uses `/courses`. All six course endpoints
|
|
below moved to `/courses`. The admin-only
|
|
`/admin/courses/:id/toggle-status` stays where it is until the backend
|
|
specs it (or until the FE migrates to PATCHing `is_active`).
|
|
|
|
The separate **course templates** API (`/admin/course-templates`,
|
|
`useAdminCourseTemplatesListQuery`, etc.) is not in the spec yet and
|
|
was not touched.
|
|
|
|
### ⚠️ Field name mismatches (apply to every courses endpoint below)
|
|
|
|
| FE / mock key (camelCase) | Spec key (snake_case) | Notes |
|
|
| ------------------------- | --------------------- | ------------------------------------------------------ |
|
|
| `image` | `cover_url` | FE reads `course.image` |
|
|
| `teacher` (nested object) | *(only `teacher_id` in list)* | FE list cards read `course.teacher` for name/avatar; need `teacher` nested on list too |
|
|
| `term` (nested object) | *(only `term_id` in list)* | FE list cards read `course.term?.title`; need `term` nested on list too |
|
|
| `template` (nested object)| *(missing entirely)* | FE-only concept (course-template link) |
|
|
| `templateId` | *(missing)* | Course-template id |
|
|
| `prerequisitesCount` | *(missing)* | Number badge on list cards |
|
|
| `startDate` / `endDate` | *(missing — read from `term.starts_at`/`ends_at`)* | List cards currently store dates flat on the course; the FE could derive from the nested `term` instead |
|
|
|
|
The mock currently writes **both** the spec key and the legacy UI key
|
|
(`coverUrl` + `image`) and includes nested `term`/`teacher` objects on
|
|
both list and detail responses so the existing list cards keep
|
|
rendering. **Required action:** backend should either include the
|
|
nested objects + UI-only counters on every course response, or the FE
|
|
needs migration. Decision pending.
|
|
|
|
### Nested `teacher` shape
|
|
|
|
The spec's detail response nests a full teacher user object using the
|
|
top-level user spec (`name`, `email`, `phone`, `roles`, `avatar_url`,
|
|
…). The UI cards already read `course.teacher.firstName` /
|
|
`course.teacher.lastName` — so this is yet another instance of the
|
|
[cross-cutting `name` vs `first_name`/`last_name`](#%EF%B8%8F-cross-cutting-name-vs-first_name--last_name)
|
|
problem. The mock writes both forms on the nested teacher.
|
|
|
|
---
|
|
|
|
## GET `/courses` — list courses (paginated)
|
|
|
|
Mock fixture: `src/services/mock/fixtures/admin-courses.js`
|
|
(`adminOfferedCourses`).
|
|
Mock route: `src/services/mock/routes/admin-courses.js`.
|
|
Frontend query: `useAdminCoursesListQuery`.
|
|
Consumed by: `src/features/admin/courses/pages/CoursesListPage.vue`,
|
|
`src/features/admin/courses/components/CourseItem.vue`.
|
|
|
|
### Spec envelope
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "OK",
|
|
"data": {
|
|
"items": [ /* course objects */ ],
|
|
"meta": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Per-item spec
|
|
|
|
| Backend key | UI key | Type |
|
|
| ------------- | ------------- | -------------- |
|
|
| `id` | `id` | number |
|
|
| `term_id` | `termId` | number |
|
|
| `teacher_id` | `teacherId` | number |
|
|
| `title` | `title` | string |
|
|
| `description` | `description` | string |
|
|
| `capacity` | `capacity` | number |
|
|
| `is_active` | `isActive` | boolean |
|
|
| `cover_url` | `coverUrl` | string \| null |
|
|
|
|
### Per-item missing — UI also needs
|
|
|
|
See "Field name mismatches" above. The list cards specifically need
|
|
`term` (nested with at least `id`, `title`), `teacher` (nested with
|
|
`id` + the user-shape so `firstName`/`lastName`/`name` are available),
|
|
`prerequisitesCount`, and `image` (or the FE migrates to `cover_url`).
|
|
`template` / `templateId` are FE-only — backend may ignore.
|
|
|
|
### Query params
|
|
|
|
Spec documents `term_id` and `per_page`. FE additionally sends
|
|
`page`, `title` (free-text), `status` (0/1), `from_date`, `to_date`.
|
|
Backend should accept or the FE filter set needs trimming.
|
|
|
|
---
|
|
|
|
## GET `/courses/:id` — show course
|
|
|
|
Mock route: same file.
|
|
Frontend query: `useAdminCourseQuery`.
|
|
Consumed by: `src/features/admin/courses/components/modals/CourseDetailsModal.vue`
|
|
(opened from the list — *not* a route page),
|
|
`src/features/admin/courses/pages/CourseTemplateFormPage.vue`
|
|
(when editing offered courses via the unified form).
|
|
|
|
### Spec envelope
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "OK",
|
|
"data": {
|
|
"id": 1,
|
|
"term_id": 1, "teacher_id": 2,
|
|
"title": "…", "description": "…", "capacity": 50, "is_active": true,
|
|
"cover_url": "…",
|
|
"term": { "id": 1, "title": "…", "description": "…", "is_active": true, "starts_at": "…", "ends_at": "…", "cover_url": "…", "created_at": "…" },
|
|
"teacher": { "id": 2, "name": "John Teacher", "email": "…", "phone": "…", "roles": ["teacher"], "avatar_url": null, "avatar_download_url": null, "created_at": "…" }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Missing — UI also needs
|
|
|
|
Same superset as list-item missing (template/templateId,
|
|
prerequisitesCount, image, nested teacher carrying
|
|
firstName/lastName/fullName).
|
|
|
|
### 404 — Not found
|
|
|
|
```json
|
|
{ "success": false, "message": "Resource not found." }
|
|
```
|
|
|
|
---
|
|
|
|
## POST `/courses` — create course
|
|
|
|
Mock route: same file.
|
|
API client: `apiAddAdminCourse`.
|
|
Query: `useAddAdminCourseMutation`.
|
|
Consumed by: course-creation form (offered-course branch of the
|
|
existing course form page / modal).
|
|
|
|
### Spec request body
|
|
|
|
```json
|
|
{
|
|
"term_id": 1,
|
|
"teacher_id": 2,
|
|
"title": "Intro to Theology",
|
|
"description": "Foundational course.",
|
|
"capacity": 50,
|
|
"is_active": true
|
|
}
|
|
```
|
|
|
|
### What the FE form sends today
|
|
|
|
| FE key (camelCase) | Wire key | Maps to spec? |
|
|
| ------------------ | -------------- | -------------------------------------- |
|
|
| `termId` | `term_id` | → `term_id` |
|
|
| `teacherId` | `teacher_id` | → `teacher_id` |
|
|
| `title` | `title` | → `title` |
|
|
| `description` | `description` | → `description` |
|
|
| `capacity` | `capacity` | → `capacity` |
|
|
| `isActive` | `is_active` | → `is_active` |
|
|
| `templateId` | `template_id` | **does not exist in spec** — FE-only |
|
|
| `imageId` | `image_id` | not in spec body — see "Image flow" below |
|
|
|
|
**Image flow** (canonical): the FE uploads the file to
|
|
`POST /media/upload`, gets a media `id` back, and submits it as
|
|
`image_id` with the course create/update body. Backend resolves
|
|
`image_id` against the media table and populates `cover_url`. No
|
|
separate per-resource upload endpoint exists — the previously
|
|
documented `POST /courses/:id/image` was a mistake and has been
|
|
removed.
|
|
|
|
### Spec response — `201 Created`
|
|
|
|
```json
|
|
{ "success": true, "message": "Course created.", "data": { /* course */ } }
|
|
```
|
|
|
|
The response should carry every list-item "UI also needs" field
|
|
(nested `term`, nested `teacher`, `image`, `template`, `templateId`,
|
|
`prerequisitesCount`) so the FE cache refresh has all it needs.
|
|
|
|
### Spec errors
|
|
|
|
- `403` `{ "success": false, "message": "This action is unauthorized." }`
|
|
- `422` `{ "message": "The given data was invalid.", "errors": { … } }`
|
|
|
|
---
|
|
|
|
## PATCH `/courses/:id` — update course
|
|
|
|
Mock route: same file.
|
|
API client: `apiUpdateAdminCourse` — **changed from `PUT` to `PATCH`**.
|
|
Query: `useUpdateAdminCourseMutation`.
|
|
|
|
### Spec request body — partial
|
|
|
|
```json
|
|
{ "title": "Intro to Theology (v2)", "capacity": 60 }
|
|
```
|
|
|
|
### What the FE form sends today
|
|
|
|
Same payload shape as create (the same form handles add + edit). The
|
|
mock now applies a true partial update — only keys present in the
|
|
payload are written.
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{ "success": true, "message": "Course updated.", "data": { /* course */ } }
|
|
```
|
|
|
|
---
|
|
|
|
## DELETE `/courses/:id` — delete course
|
|
|
|
Mock route: same file.
|
|
API client: `apiDeleteAdminCourse`.
|
|
Query: `useDeleteAdminCourseMutation`.
|
|
Consumed by: `src/features/admin/courses/pages/CoursesListPage.vue`
|
|
(via `ConfirmModal`).
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{ "success": true, "message": "Course deleted.", "data": null }
|
|
```
|
|
|
|
Previous mock returned a Persian message under `data.message` —
|
|
replaced by the envelope. The FE only invalidates the list cache on
|
|
success.
|
|
|
|
---
|
|
|
|
## Sessions (`/sessions`) — global notes
|
|
|
|
The previous FE wiring used `/admin/sessions`; the spec uses
|
|
`/sessions`. All six session endpoints below moved to `/sessions`. The
|
|
admin-only `/admin/sessions/:id/toggle-status` and
|
|
`/admin/sessions/:sessionId/attendances` stay where they are until the
|
|
backend specs them.
|
|
|
|
### ⚠️ Structural mismatch: course vs course-template
|
|
|
|
The spec says a session belongs to an **offered course** (`course_id`).
|
|
The FE models sessions as belonging to a **course template**
|
|
(`course_template_id`), where a single session can be reused across
|
|
multiple terms/offered courses (`session.used_in_terms`).
|
|
|
|
- The mock now stores **both** `courseId` (spec) and `courseTemplate`
|
|
(UI nested object). When the FE creates a session via
|
|
`course_template_id`, the mock auto-derives `course_id` from the
|
|
first offered course with that template (best-effort).
|
|
- Backend should decide: either expose `course_template_id` on the
|
|
session endpoints, or the FE needs to migrate to picking an offered
|
|
course directly when creating a session. Decision pending.
|
|
|
|
### ⚠️ Enum mismatch: `type` (spec) vs `session_type` (FE)
|
|
|
|
Spec values for `type`: **`online` | `offline` | `content`** (3 values).
|
|
FE values for `session_type` (enum `SESSION_TYPE`):
|
|
**`in_person` | `online` | `video` | `audio` | `text` | `slide` | `pdf`**
|
|
(7 values).
|
|
|
|
The mock derives `type` from `session_type` like this (see
|
|
`SESSION_TYPE_TO_SPEC` in the fixture):
|
|
|
|
| FE `session_type` | Spec `type` |
|
|
| ----------------------------------- | ----------- |
|
|
| `in_person` | `offline` |
|
|
| `online` | `online` |
|
|
| `video` / `audio` / `text` / `slide` / `pdf` | `content` |
|
|
|
|
The FE form picks from the 7-value enum and renders different config
|
|
blocks per value (`SessionFormPage.vue`). Backend should either:
|
|
|
|
1. Adopt the 7-value enum and map to its internal 3-value domain
|
|
server-side, **or**
|
|
2. Add a sub-type column (`session_subtype` etc.) so the FE can still
|
|
pick `video`/`audio`/`pdf` while `type` stays in the 3-value
|
|
spec set.
|
|
|
|
Decision pending.
|
|
|
|
### ⚠️ Flat fields vs `session_config`
|
|
|
|
The spec **flattens** what the FE keeps nested in `session_config`:
|
|
|
|
| Spec key (top-level) | FE key (nested) |
|
|
| -------------------- | --------------------------------------- |
|
|
| `starts_at` | `session_config.start_time` |
|
|
| `location` | `session_config.location` (in_person) |
|
|
| `link` | `session_config.meeting_link` (online) |
|
|
| *(missing)* | `session_config.platform` (online) |
|
|
| *(missing)* | `session_config.min_watched_percent` (video/audio) |
|
|
| *(missing)* | `session_config.min_read_percent` (text/slide/pdf) |
|
|
| *(missing)* | `session_config.must_complete_before_next` |
|
|
|
|
The mock now writes the spec keys flat AND keeps the legacy
|
|
`session_config` nested block so `SessionDetailsModal.vue` (which
|
|
reads `session.session_config.*`) keeps rendering. **Required
|
|
action:** backend should either accept the nested `session_config` and
|
|
return it back, or the FE detail modal needs to be migrated to read
|
|
the flat keys + the missing config knobs need a new home (probably a
|
|
JSON column).
|
|
|
|
### Other UI-only fields the spec doesn't cover
|
|
|
|
`image`, `duration_minutes`, `order`, `materials`,
|
|
`used_in_terms`, `course_template` (nested). The FE list cards and
|
|
detail modal all read these — backend should include them or the FE
|
|
needs migration. The mock keeps them all.
|
|
|
|
---
|
|
|
|
## GET `/sessions` — list sessions (paginated)
|
|
|
|
Mock fixture: `src/services/mock/fixtures/admin-sessions.js`
|
|
(`adminSessions`).
|
|
Mock route: `src/services/mock/routes/admin-sessions.js`.
|
|
Frontend query: `useAdminSessionsListQuery`.
|
|
Consumed by: `src/features/admin/sessions/pages/SessionsListPage.vue`,
|
|
`src/features/admin/sessions/components/SessionItem.vue`.
|
|
|
|
### Spec envelope
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "OK",
|
|
"data": {
|
|
"items": [ /* session objects */ ],
|
|
"meta": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Per-item spec
|
|
|
|
| Backend key | UI key | Type |
|
|
| ------------- | ------------- | ----------------------------------------------- |
|
|
| `id` | `id` | number |
|
|
| `course_id` | `courseId` | number |
|
|
| `title` | `title` | string |
|
|
| `description` | `description` | string |
|
|
| `type` | `type` | enum: `online` \| `offline` \| `content` |
|
|
| `starts_at` | `startsAt` | string (ISO with tz) |
|
|
| `location` | `location` | string \| null |
|
|
| `link` | `link` | string \| null |
|
|
|
|
### Per-item missing — UI also needs
|
|
|
|
See "Structural", "Enum", and "Flat vs session_config" callouts
|
|
above. Concretely the list card reads: `image`, `courseTemplate.title`,
|
|
`durationMinutes`, `sessionType` / `sessionTypeFa`, `usedInTerms[]`.
|
|
None of those are in the spec yet.
|
|
|
|
### Query params
|
|
|
|
Spec documents `course_id` + `per_page`. FE additionally sends
|
|
`title`, `course_template_id`, `session_type`, `from_date`, `to_date`,
|
|
`page`. Backend should accept or the FE filter set needs trimming.
|
|
|
|
---
|
|
|
|
## GET `/sessions/:id` — show session
|
|
|
|
Mock route: same file.
|
|
Frontend query: `useAdminSessionQuery`.
|
|
Consumed by: `src/features/admin/sessions/components/modals/SessionDetailsModal.vue`,
|
|
`src/features/admin/sessions/pages/SessionFormPage.vue` (edit mode).
|
|
|
|
### Spec envelope
|
|
|
|
```json
|
|
{
|
|
"success": true, "message": "OK",
|
|
"data": {
|
|
"id": 1, "course_id": 1,
|
|
"title": "…", "description": "…", "type": "online",
|
|
"starts_at": "…", "location": null, "link": "…",
|
|
"media": [ { "id": 15, "collection_name": "attachments", "file_name": "…", "mime_type": "…", "file_size": 204800, "url": "…", "download_url": "…" } ],
|
|
"course": { /* full offered-course object, same shape as GET /courses/:id flat fields */ }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Missing — UI also needs
|
|
|
|
Detail modal needs: `session_config.*` (with the config knobs not in
|
|
spec — `platform`, `min_watched_percent`, etc.),
|
|
`course_template` (nested), `image`, `duration_minutes`, `order`,
|
|
`materials[]`, `used_in_terms[]`. The mock writes all of them.
|
|
|
|
The spec's `media[]` and the FE's `materials[]` are **different
|
|
arrays** — `media` is the new attachments collection (PDFs, slides,
|
|
recordings); `materials` is a legacy FE concept. Backend can either
|
|
collapse them into one field or keep both — flagged for decision.
|
|
|
|
### 404 — Not found
|
|
|
|
```json
|
|
{ "success": false, "message": "Resource not found." }
|
|
```
|
|
|
|
---
|
|
|
|
## POST `/sessions` — create session
|
|
|
|
Mock route: same file.
|
|
API client: `apiAddAdminSession`.
|
|
Query: `useAddAdminSessionMutation`.
|
|
Consumed by: `src/features/admin/sessions/pages/SessionFormPage.vue` (add).
|
|
|
|
### Spec request body
|
|
|
|
```json
|
|
{
|
|
"course_id": 1,
|
|
"title": "Week 1 — Overview",
|
|
"description": "Opening session.",
|
|
"type": "online",
|
|
"starts_at": "2026-03-25T18:00:00+03:30",
|
|
"location": null,
|
|
"link": "https://meet.example.com/abc"
|
|
}
|
|
```
|
|
|
|
### What the FE form sends today
|
|
|
|
| FE key (camelCase) | Wire key | Maps to spec? |
|
|
| ------------------------- | --------------------------- | ------------------------------------------------ |
|
|
| `title` | `title` | → `title` |
|
|
| `description` | `description` | → `description` |
|
|
| `courseTemplateId` | `course_template_id` | **not in spec** — see structural callout |
|
|
| `sessionType` | `session_type` | maps to spec `type` via the table above |
|
|
| `durationMinutes` | `duration_minutes` | missing in spec |
|
|
| `order` | `order` | missing in spec |
|
|
| `sessionConfig.startTime` | `session_config.start_time` | maps to spec `starts_at` |
|
|
| `sessionConfig.location` | `session_config.location` | maps to spec `location` |
|
|
| `sessionConfig.meetingLink` | `session_config.meeting_link` | maps to spec `link` |
|
|
| `sessionConfig.platform` | `session_config.platform` | missing in spec |
|
|
| `sessionConfig.minWatchedPercent` | `session_config.min_watched_percent` | missing in spec |
|
|
| `sessionConfig.minReadPercent` | `session_config.min_read_percent` | missing in spec |
|
|
| `sessionConfig.mustCompleteBeforeNext` | `session_config.must_complete_before_next` | missing in spec |
|
|
| `imageId` | `image_id` | media id from `POST /media/upload`; backend should set `cover_url` |
|
|
| `materials` | `materials[]` | array of media ids (each from `POST /media/upload`); backend attaches them as session media; overlaps with spec `media[]` — see callout |
|
|
|
|
The mock accepts both naming conventions on input. **The FE form
|
|
needs a flat `course_id` selector** to fully match the spec (or
|
|
backend must accept `course_template_id`).
|
|
|
|
### Spec response — `201 Created`
|
|
|
|
```json
|
|
{ "success": true, "message": "Session created.", "data": { /* session */ } }
|
|
```
|
|
|
|
### Spec errors
|
|
|
|
- `422` `{ "message": "The given data was invalid.", "errors": { "course_id": [...], "title": [...], "type": [...] } }`
|
|
|
|
---
|
|
|
|
## PATCH `/sessions/:id` — update session
|
|
|
|
Mock route: same file.
|
|
API client: `apiUpdateAdminSession` — **changed from `PUT` to `PATCH`**.
|
|
Query: `useUpdateAdminSessionMutation`.
|
|
|
|
### Spec request body — partial
|
|
|
|
```json
|
|
{ "title": "…", "starts_at": "…" }
|
|
```
|
|
|
|
### What the FE sends today
|
|
|
|
Same payload shape as create. The mock now applies a true partial
|
|
update — only keys present in the payload are written.
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{ "success": true, "message": "Session updated.", "data": { /* session */ } }
|
|
```
|
|
|
|
---
|
|
|
|
## DELETE `/sessions/:id` — delete session
|
|
|
|
Mock route: same file.
|
|
API client: `apiDeleteAdminSession`.
|
|
Query: `useDeleteAdminSessionMutation`.
|
|
|
|
### Spec response — `200 OK`
|
|
|
|
```json
|
|
{ "success": true, "message": "Session deleted.", "data": null }
|
|
```
|
|
|
|
Previous mock returned a Persian message under `data.message` —
|
|
replaced by the envelope. The FE only invalidates the list cache on
|
|
success.
|
|
|
|
---
|
|
|
|
<!-- Append further routes below as they are wired up. -->
|