diff --git a/.env b/.env index 2be7680..7c747cc 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -VITE_API_BASE_URL= -VITE_USE_MOCKS=true +VITE_API_BASE_URL=https://tripwisedaily.ir/api +VITE_USE_MOCKS=false diff --git a/docs/backend-api-todo.md b/docs/backend-api-todo.md new file mode 100644 index 0000000..56a0a27 --- /dev/null +++ b/docs/backend-api-todo.md @@ -0,0 +1,1268 @@ +# 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. + +--- + + diff --git a/package-lock.json b/package-lock.json index fd14004..cdb878c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@tanstack/vue-query-devtools": "^5.62.2", "@tinymce/tinymce-vue": "^4.0.7", "axios": "^1.12.2", + "jalaali-js": "^1.2.8", "lodash": "^4.18.1", "pinia": "^3.0.3", "tinymce": "^8.5.0", diff --git a/package.json b/package.json index bc83a12..1fc9fb8 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@tanstack/vue-query-devtools": "^5.62.2", "@tinymce/tinymce-vue": "^4.0.7", "axios": "^1.12.2", + "jalaali-js": "^1.2.8", "lodash": "^4.18.1", "pinia": "^3.0.3", "tinymce": "^8.5.0", diff --git a/src/components/form/DatePickerField.vue b/src/components/form/DatePickerField.vue index 8ebb6d2..8495404 100644 --- a/src/components/form/DatePickerField.vue +++ b/src/components/form/DatePickerField.vue @@ -4,7 +4,7 @@