From 9660e3fe8fd5ce8b639a5fecd4a8108bd3973eef Mon Sep 17 00:00:00 2001 From: sajjadtalkhabi <97734061+sajjadtalkhabi@users.noreply.github.com> Date: Thu, 28 May 2026 15:52:48 +0330 Subject: [PATCH] fix: linter --- .eslintrc.cjs | 8 +- .gitea/workflows/deploy.yml | 4 +- .husky/pre-commit | 6 +- .lintstagedrc.json | 8 +- .vscode/extensions.json | 1 - docs/backend-api-todo.md | 969 ++++++++---------- docs/backend-vs-ui-gaps.md | 28 +- index.html | 2 +- package-lock.json | 672 ++++++++++++ package.json | 1 + src/App.vue | 6 +- src/components/Tinymce.vue | 8 +- src/components/form/CheckboxField.vue | 4 +- src/components/form/ImageUploader.vue | 2 +- .../components/AssignmentsFilters.vue | 10 +- .../components/modals/AddAssignmentModal.vue | 3 +- .../modals/AssignmentDetailsModal.vue | 2 +- .../modals/AssignmentSubmissionsModal.vue | 1 - .../modals/AddSessionToCourseModal.vue | 1 - .../admin/exams/components/ExamsFilters.vue | 10 +- .../modals/ExamParticipantDetailsModal.vue | 6 +- .../components/modals/SendMessageModal.vue | 2 +- .../components/modals/ServiceDetailsModal.vue | 2 +- .../modals/SessionAttendanceModal.vue | 1 - .../components/modals/SessionDetailsModal.vue | 4 +- .../modals/UserVerificationDetailsModal.vue | 1 - .../EducationalInformation.vue | 2 +- .../ProfessionalInformation.vue | 2 +- .../studentRegister/SkillAssessmentPart7.vue | 4 +- .../auth/store/student-registration.js | 2 +- .../student/exams/pages/StudentExamPage.vue | 2 +- .../pages/StudentSessionDetailsPage.vue | 2 +- .../terms/components/StudentTermSummary.vue | 5 +- src/plugins/tinymce/importTinymce.js | 66 +- src/plugins/tinymce/langs/fa_IR.js | 864 +++++++++------- src/plugins/tinymce/tinymce.js | 6 +- src/services/api/http.js | 4 +- src/services/mock/routes/admin-users.js | 12 +- src/services/query/auth.js | 3 +- src/store/auth.js | 2 +- src/styles/plugins/tinymce.css | 18 +- 41 files changed, 1669 insertions(+), 1087 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9640921..fa4a58c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,7 +6,13 @@ module.exports = { es2021: true, }, plugins: ['prettier', 'unicorn', 'import', 'waterfall'], - ignorePatterns: ['node_modules/', 'dist/', 'build/', '*.min.js', 'src/components/icons/icon-names.d.ts'], + ignorePatterns: [ + 'node_modules/', + 'dist/', + 'build/', + '*.min.js', + 'src/components/icons/icon-names.d.ts', + ], parserOptions: { ecmaVersion: 2021, sourceType: 'module', diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 8188dca..10cf5be 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -11,9 +11,9 @@ jobs: env: IMAGE: banu-front CONTAINER: banu-front - PORT: "8080" + PORT: '8080' VITE_API_BASE_URL: http://109.122.252.86/api - VITE_USE_MOCKS: "false" + VITE_USE_MOCKS: 'false' steps: - name: Clone repository at current commit run: | diff --git a/.husky/pre-commit b/.husky/pre-commit index d4dbd72..d24fdfc 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh -# Use the locally-installed lint-staged so this works under environments -# where npm/npx is broken (e.g. GitHub Desktop's bundled git env). -# exec ./node_modules/.bin/lint-staged +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/.lintstagedrc.json b/.lintstagedrc.json index eb0470e..a9a58bb 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,4 +1,4 @@ -{ - "*.{js,vue}": "eslint --fix", - "*.scss": "prettier --write" -} +{ + "*.{js,vue}": "eslint --fix", + "*.scss": "prettier --write" +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0c017a1..d53c84d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,4 +1,3 @@ - { "recommendations": [ "esbenp.prettier-vscode", diff --git a/docs/backend-api-todo.md b/docs/backend-api-todo.md index 56a0a27..5c24224 100644 --- a/docs/backend-api-todo.md +++ b/docs/backend-api-todo.md @@ -1,87 +1,46 @@ # 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: +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. +- **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. +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 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.). +- 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`. +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. +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. +**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`). +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. +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). +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 @@ -101,76 +60,66 @@ form, and the student-register media/personal-info components). } ``` -`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`. +`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 | +| 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. +> **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`. +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. +> 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 | +| 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` | +| 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 @@ -196,7 +145,7 @@ Frontend query: `useGetMeQuery` in `src/services/query/auth.js`. "address": { "address": "...", "province": { "id": 1, "name": "تهران" }, - "city": { "id": 11, "name": "تهران" } + "city": { "id": 11, "name": "تهران" } }, "profile": { "bio": "...", @@ -218,11 +167,7 @@ Frontend query: `useGetMeQuery` in `src/services/query/auth.js`. ## 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`. +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 @@ -231,7 +176,9 @@ Consumed by: `src/features/admin/users/pages/UsersListPage.vue`, "success": true, "message": "OK", "data": { - "items": [ /* user objects */ ], + "items": [ + /* user objects */ + ], "meta": { "current_page": 1, "per_page": 20, @@ -242,43 +189,37 @@ Consumed by: `src/features/admin/users/pages/UsersListPage.vue`, } ``` -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. +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 | +| 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 | +| 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). +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 @@ -286,7 +227,9 @@ Consumed by: `src/features/admin/users/components/modals/UserDetailsModal.vue` { "success": true, "message": "OK", - "data": { /* user object */ } + "data": { + /* user object */ + } } ``` @@ -296,32 +239,28 @@ 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: +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 | +| 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. +> 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 @@ -332,17 +271,13 @@ fields **plus** the nested `address` and `profile` blocks: } ``` -The FE `findOrThrow` mock throws a 404; the toast/error layer surfaces -the `message` field — keep that key name. +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). +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 @@ -358,31 +293,27 @@ Consumed by: `src/features/admin/users/pages/UserFormPage.vue` (add mode). ### 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: +`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`) | +| 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. +> **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` @@ -390,14 +321,13 @@ with `→ spec`; the rest have no spec equivalent yet: { "success": true, "message": "User created.", - "data": { /* user object, same shape as GET /admin/users/:id */ } + "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. +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` @@ -410,19 +340,13 @@ extra round-trip. } ``` -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. +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). +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 @@ -437,15 +361,11 @@ 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: +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. +- `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` @@ -453,22 +373,19 @@ mode). Notable differences: { "success": true, "message": "User updated.", - "data": { /* user object, same shape as GET /admin/users/:id */ } + "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. +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`). +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` @@ -480,25 +397,15 @@ Consumed by: `src/features/admin/users/pages/UsersListPage.vue` } ``` -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 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`. +> 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`). +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 @@ -516,56 +423,40 @@ The FE now sends `{ roles: [targetRoleName] }` exactly per spec. { "success": true, "message": "Roles updated.", - "data": { /* user object, same shape as GET /admin/users/:id */ } + "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 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: [...] }`. +> 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. +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 | +| 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. +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`. +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 @@ -574,7 +465,9 @@ Consumed by: `src/features/admin/terms/pages/TermsListPage.vue`, "success": true, "message": "OK", "data": { - "items": [ /* term objects */ ], + "items": [ + /* term objects */ + ], "meta": { "current_page": 1, "per_page": 20, "total": 2, "last_page": 1 } } } @@ -582,26 +475,22 @@ Consumed by: `src/features/admin/terms/pages/TermsListPage.vue`, ### 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) | +| 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. +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. +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 @@ -609,26 +498,27 @@ drop them from the FE. { "message": "Unauthenticated." } ``` -The global HTTP layer already redirects to `/login` on 401 (see -`addUnauthorizeInterceptor` in `src/services/api/http.js`). +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`. +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 */ } } +{ + "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`). +Same fields as a list item. The detail modal needs the same UI-only keys (`image`, `startDate`, `endDate`, `studentsCount`, `coursesCount`). ### 404 — Not found @@ -640,10 +530,7 @@ keys (`image`, `startDate`, `endDate`, `studentsCount`, `coursesCount`). ## POST `/terms` — create term -Mock route: same file. -API client: `apiAddAdminTerm`. -Query: `useAddAdminTermMutation`. -Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (add). +Mock route: same file. API client: `apiAddAdminTerm`. Query: `useAddAdminTermMutation`. Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (add). ### Spec request body @@ -659,35 +546,31 @@ Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (add). ### 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 | +| 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. +**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. +`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 */ } } +{ + "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. +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 @@ -700,10 +583,7 @@ 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). +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 @@ -713,14 +593,18 @@ Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (edit). ### 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. +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 */ } } +{ + "success": true, + "message": "Term updated.", + "data": { + /* term */ + } +} ``` ### Spec errors @@ -731,11 +615,7 @@ payload are left unchanged on the stored term. ## 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`). +Mock route: same file. API client: `apiDeleteAdminTerm`. Query: `useDeleteAdminTermMutation`. Consumed by: `src/features/admin/terms/pages/TermsListPage.vue` (via `ConfirmModal`). ### Spec response — `200 OK` @@ -747,63 +627,39 @@ Consumed by: `src/features/admin/terms/pages/TermsListPage.vue` - `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. +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`). +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. +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 | +| 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. +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. +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`. +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 @@ -812,7 +668,9 @@ Consumed by: `src/features/admin/courses/pages/CoursesListPage.vue`, "success": true, "message": "OK", "data": { - "items": [ /* course objects */ ], + "items": [ + /* course objects */ + ], "meta": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 } } } @@ -833,28 +691,17 @@ Consumed by: `src/features/admin/courses/pages/CoursesListPage.vue`, ### 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. +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. +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). +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 @@ -864,20 +711,40 @@ Consumed by: `src/features/admin/courses/components/modals/CourseDetailsModal.vu "message": "OK", "data": { "id": 1, - "term_id": 1, "teacher_id": 2, - "title": "…", "description": "…", "capacity": 50, "is_active": true, + "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": "…" } + "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). +Same superset as list-item missing (template/templateId, prerequisitesCount, image, nested teacher carrying firstName/lastName/fullName). ### 404 — Not found @@ -889,11 +756,7 @@ firstName/lastName/fullName). ## 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). +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 @@ -910,34 +773,32 @@ existing course form page / modal). ### 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 | +| 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. +**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 */ } } +{ + "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. +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 @@ -948,9 +809,7 @@ The response should carry every list-item "UI also needs" field ## PATCH `/courses/:id` — update course -Mock route: same file. -API client: `apiUpdateAdminCourse` — **changed from `PUT` to `PATCH`**. -Query: `useUpdateAdminCourseMutation`. +Mock route: same file. API client: `apiUpdateAdminCourse` — **changed from `PUT` to `PATCH`**. Query: `useUpdateAdminCourseMutation`. ### Spec request body — partial @@ -960,25 +819,25 @@ Query: `useUpdateAdminCourseMutation`. ### 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. +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 */ } } +{ + "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`). +Mock route: same file. API client: `apiDeleteAdminCourse`. Query: `useDeleteAdminCourseMutation`. Consumed by: `src/features/admin/courses/pages/CoursesListPage.vue` (via `ConfirmModal`). ### Spec response — `200 OK` @@ -986,59 +845,37 @@ Consumed by: `src/features/admin/courses/pages/CoursesListPage.vue` { "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. +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. +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 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. +- 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). +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): +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` | +| 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: +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. +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. @@ -1046,41 +883,27 @@ Decision pending. 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` | +| 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). +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. +`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`. +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 @@ -1089,7 +912,9 @@ Consumed by: `src/features/admin/sessions/pages/SessionsListPage.vue`, "success": true, "message": "OK", "data": { - "items": [ /* session objects */ ], + "items": [ + /* session objects */ + ], "meta": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 } } } @@ -1097,65 +922,69 @@ Consumed by: `src/features/admin/sessions/pages/SessionsListPage.vue`, ### 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 | +| 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. +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. +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). +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", + "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 */ } + "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. +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. +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 @@ -1167,10 +996,7 @@ collapse them into one field or keep both — flagged for decision. ## POST `/sessions` — create session -Mock route: same file. -API client: `apiAddAdminSession`. -Query: `useAddAdminSessionMutation`. -Consumed by: `src/features/admin/sessions/pages/SessionFormPage.vue` (add). +Mock route: same file. API client: `apiAddAdminSession`. Query: `useAddAdminSessionMutation`. Consumed by: `src/features/admin/sessions/pages/SessionFormPage.vue` (add). ### Spec request body @@ -1188,32 +1014,36 @@ Consumed by: `src/features/admin/sessions/pages/SessionFormPage.vue` (add). ### 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 | +| 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`). +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 */ } } +{ + "success": true, + "message": "Session created.", + "data": { + /* session */ + } +} ``` ### Spec errors @@ -1224,9 +1054,7 @@ backend must accept `course_template_id`). ## PATCH `/sessions/:id` — update session -Mock route: same file. -API client: `apiUpdateAdminSession` — **changed from `PUT` to `PATCH`**. -Query: `useUpdateAdminSessionMutation`. +Mock route: same file. API client: `apiUpdateAdminSession` — **changed from `PUT` to `PATCH`**. Query: `useUpdateAdminSessionMutation`. ### Spec request body — partial @@ -1236,22 +1064,25 @@ Query: `useUpdateAdminSessionMutation`. ### 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. +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 */ } } +{ + "success": true, + "message": "Session updated.", + "data": { + /* session */ + } +} ``` --- ## DELETE `/sessions/:id` — delete session -Mock route: same file. -API client: `apiDeleteAdminSession`. -Query: `useDeleteAdminSessionMutation`. +Mock route: same file. API client: `apiDeleteAdminSession`. Query: `useDeleteAdminSessionMutation`. ### Spec response — `200 OK` @@ -1259,9 +1090,7 @@ Query: `useDeleteAdminSessionMutation`. { "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. +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/docs/backend-vs-ui-gaps.md b/docs/backend-vs-ui-gaps.md index 75970b5..9c056e1 100644 --- a/docs/backend-vs-ui-gaps.md +++ b/docs/backend-vs-ui-gaps.md @@ -3,6 +3,7 @@ Source of truth: the Postman collection covering Terms, Courses, Sessions, Exams, Homeworks, Media (2026-05). This doc lists every place where backend and FE disagree on shape, plus FE-side concepts the backend doc has no slot for. Each item needs a product/design call before we wire it up. Conventions in the rest of this doc: + - **B → FE** means the backend exposes a field/endpoint that the FE doesn't surface yet. - **FE → B** means the FE shows/sends a field the backend doc doesn't accept. - **shape diff** means both sides handle the concept but in different shapes (enum values, nesting, naming). @@ -14,7 +15,7 @@ Conventions in the rest of this doc: ### Endpoints | Backend | FE today | Status | -|---|---|---| +| --- | --- | --- | | `GET /terms?per_page&active_only` | `getTermsList` | aligned | | `GET /terms/:id` | `showTerm` | aligned | | `POST /terms` | `addNewTerm` | aligned | @@ -28,7 +29,7 @@ Conventions in the rest of this doc: ### Fields | Backend → FE | FE has it as | Notes | -|---|---|---| +| --- | --- | --- | | `starts_at`, `ends_at` (ISO datetime) | `startDate`, `endDate` | Naming diff; we send `starts_at`/`ends_at` in the mock already — confirm the FE form should rename or keep an adapter. | | `cover_url` (read) + `cover_media_id` (write) | `coverUrl` / `coverMediaId` | aligned in shape, just camelCase. | | `created_at` | `createdAt` | aligned. | @@ -36,6 +37,7 @@ Conventions in the rest of this doc: | `description` | `description` | aligned. | **FE → B** (need decision): + - `studentsCount`, `coursesCount` — computed UI counts. Backend response doesn't include them. Either compute client-side or ask backend for `?include=counts`. - `image` (UI alias of cover) — FE keeps this for a fallback. Backend response only has `cover_url`. @@ -46,7 +48,7 @@ Conventions in the rest of this doc: ### Endpoints | Backend | FE today | Status | -|---|---|---| +| --- | --- | --- | | `GET /courses?term_id&per_page` | `getCoursesList` | aligned (drop `/admin/` prefix in `endpoints.js`). | | `GET /courses/:id` | `showCourse` | aligned. Backend response includes nested `term` and `teacher` — FE already reads `course.term` / `course.teacher`, good. | | `POST /courses` | `addNewCourse` | aligned. Backend currently requires `term_id` (422 example), but per user instruction the standalone (template-tab) flow must allow `term_id: null`. **Decide:** ask backend to make `term_id` nullable, or refuse to submit until a term is picked. | @@ -59,7 +61,7 @@ Conventions in the rest of this doc: ### Fields | Backend ↔ FE | Notes | -|---|---| +| --- | --- | | `term_id` ⇄ `termId` | aligned. Nullable in CourseFormPage, required in AddOfferedCourseModal — schema reflects this. | | `teacher_id` ⇄ `teacherId` | aligned. | | `capacity` ⇄ `capacity` | aligned. | @@ -69,6 +71,7 @@ Conventions in the rest of this doc: | Nested `term`, `teacher` on show response | FE already reads. | **FE → B** (need decision): + - `sessionsCount` — number-of-sessions field on the create form. Backend has nothing. Either compute server-side from related sessions, or drop the field. - `prerequisites` (array of { courseId, course }) — backend has no prerequisite relation. Drop or ask backend for it. - `contentType` (video/voice/text) + `contentMediaId` — single course-level content file. Backend treats files only as session media. Decide whether course-level content should move to "intro session" or stay a course concept. @@ -81,7 +84,7 @@ Conventions in the rest of this doc: ### Endpoints | Backend | FE today | Status | -|---|---|---| +| --- | --- | --- | | `GET /sessions?course_id&per_page` | `getSessionsList` | aligned. | | `GET /sessions/:id` | `showSession` | aligned. | | `POST /sessions` | `addNewSession` | aligned. | @@ -93,7 +96,7 @@ Conventions in the rest of this doc: ### Fields — biggest gap of all five resources | Backend | FE today | Notes | -|---|---|---| +| --- | --- | --- | | `type` enum: `online \| offline \| content` | `sessionType` enum: `in_person`, `online`, `video`, `audio`, `text`, `slide`, `pdf` | **Shape diff.** FE has 7 values; backend has 3. There's an existing `SESSION_TYPE_TO_SPEC` mapper in `services/mock/fixtures/admin-sessions.js`. Decide whether the FE keeps the richer 7-value enum (and we map down to backend's 3) or collapses. | | `starts_at`, `location`, `link` | All three live **inside** `form.sessionConfig.*` plus also derived to top-level `startsAt` / `location` / `link` in the mock | **Structural diff.** Backend wants flat fields; FE form nests them under `sessionConfig` keyed by `sessionType`. The mock derives top-level from `sessionConfig.*` for show payloads. Decide whether the FE form should flatten the schema to match backend or keep the conditional-by-type config UI. | | `media_ids[]` (write) / `media[]` (read with `collection_name`, `file_name`, `mime_type`, `file_size`, `url`, `download_url`) | `materials[]` with `{ fileId, isRequired, type, title, order }` | **Shape diff.** Backend's media rows are typed by upload-purpose (video/voice/pdf/slide/attachment); FE has its own `type` enum. Decide which shape the FE keeps. | @@ -108,7 +111,7 @@ Conventions in the rest of this doc: ### Endpoints | Backend | FE today | Status | -|---|---|---| +| --- | --- | --- | | `GET /exams/:id` (with questions+options) | `showExam` | aligned. | | `POST /exams` | `addNewExam` | aligned. | | `PATCH /exams/:id` | `updateExam` | aligned. | @@ -122,7 +125,7 @@ Conventions in the rest of this doc: ### Fields | Backend ↔ FE | Notes | -|---|---| +| --- | --- | --- | | `session_id` ⇄ `sessionId` | aligned. | | `title` ⇄ `title` | aligned. | | `description` ⇄ `description` | aligned. | @@ -131,6 +134,7 @@ Conventions in the rest of this doc: | Backend question shape: `{ question_text, position, options: [{ option_text, is_correct }] }` | FE: `{ title, score, correctAnswerId, answers: [{ id, title }] }` | **Shape diff.** Backend hides `is_correct` from public reads (only on add). FE concept of `score` (per-question weighting) has no backend slot. Decide: keep FE scoring (ask backend to store) or drop. | **FE → B** (need decision): + - `durationMinutes` — no backend slot. - `randomize` — no backend slot. - `endDate` / `startDate` — exam validity window, no backend slot. @@ -143,7 +147,7 @@ Conventions in the rest of this doc: ### Endpoints | Backend | FE today | Status | -|---|---|---| +| --- | --- | --- | | `POST /homeworks` | `addNewAssignment` | aligned (URL renamed to `/homeworks`; FE key name kept). | | `PATCH /homeworks/:id` | `updateAssignment` | aligned. | | `DELETE /homeworks/:id` | `deleteAssignment` | aligned. | @@ -155,7 +159,7 @@ Conventions in the rest of this doc: ### Fields | Backend ↔ FE | Notes | -|---|---| +| --- | --- | --- | | `session_id` ⇄ `sessionId` | aligned. | | `title`, `description` | aligned. | | `deadline` (single datetime) | FE: `startDate` + `endDate` + computed `durationDays` | **Shape diff.** Backend has one deadline; FE has a window. Decide: drop start/end and use single deadline, or ask backend to add a window. | @@ -166,6 +170,7 @@ Conventions in the rest of this doc: | Submission `reviewed_at` (read) | — | Backend provides; FE doesn't surface. | **FE → B** (need decision): + - `priority` enum (`mandatory \| optional`) — no backend slot. - `submissionsCount` — derived count, no backend slot. @@ -176,7 +181,7 @@ Conventions in the rest of this doc: ### Endpoints | Backend | FE today | Status | -|---|---|---| +| --- | --- | --- | | `POST /media` (multipart with `purpose` + `file`) | `uploadMedia` | aligned URL & method. | | `GET /media/:id/download` | — | **Backend → FE.** Add as `downloadMedia`. | | `DELETE /media/:id` | — | **Backend → FE.** Add as `deleteMedia`. | @@ -194,6 +199,7 @@ Conventions in the rest of this doc: ### Snake_case vs camelCase Backend wire format is snake_case throughout. FE today reads camelCase (e.g., the mock returns `coverUrl`, `isActive`). When the real backend lands, the FE will either need: + - a HTTP-layer transformer (camelize on response, snake_case on request), or - camelCase field aliases on the backend serializer. diff --git a/index.html b/index.html index 09d93c1..50d0df9 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - +
diff --git a/package-lock.json b/package-lock.json index b3ad252..7e0d180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "eslint-plugin-unicorn": "^55.0.0", "eslint-plugin-vue": "^9.15.1", "eslint-plugin-waterfall": "^1.0.1", + "lint-staged": "^15.5.2", "postcss-html": "^1.8.0", "prettier": "2.8.8", "sass": "^1.83.0", @@ -1927,6 +1928,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2432,6 +2449,136 @@ "node": ">=0.8.0" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2459,6 +2606,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2950,6 +3104,19 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -3597,6 +3764,37 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3898,6 +4096,19 @@ "node": ">= 0.4" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3935,6 +4146,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -4265,6 +4489,16 @@ "entities": "^4.4.0" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/husky": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.0.tgz", @@ -4721,6 +4955,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -4965,6 +5212,19 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4972,6 +5232,75 @@ "dev": true, "license": "MIT" }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5008,6 +5337,101 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5057,6 +5481,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5115,6 +5546,32 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5307,6 +5764,35 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -5442,6 +5928,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5623,6 +6125,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pinia": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", @@ -6151,6 +6666,39 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6606,6 +7154,16 @@ "node": ">= 0.4" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -6703,6 +7261,19 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -7830,6 +8401,91 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7860,6 +8516,22 @@ "node": ">=12" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index a9e2147..bb49a0c 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "eslint-plugin-unicorn": "^55.0.0", "eslint-plugin-vue": "^9.15.1", "eslint-plugin-waterfall": "^1.0.1", + "lint-staged": "^15.5.2", "postcss-html": "^1.8.0", "prettier": "2.8.8", "sass": "^1.83.0", diff --git a/src/App.vue b/src/App.vue index 94bbfbb..3ae8ca5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,14 +8,14 @@ import { storeToRefs } from 'pinia' import { computed, watch } from 'vue' import { useRoute } from 'vue-router' -import { useModalStore } from '@/store/modal' import { useAuthStore } from '@/store/auth' -import { useGetMeQuery } from '@/services/query/auth' -import { tokenService } from '@/services/api/token-service' +import { useModalStore } from '@/store/modal' import AdminLayout from '@/layouts/AdminLayout.vue' +import { useGetMeQuery } from '@/services/query/auth' import PublicLayout from '@/layouts/PublicLayout.vue' import StudentLayout from '@/layouts/StudentLayout.vue' import ConfirmModal from '@/components/ConfirmModal.vue' +import { tokenService } from '@/services/api/token-service' import { VueQueryDevtools } from '@tanstack/vue-query-devtools' const route = useRoute() diff --git a/src/components/Tinymce.vue b/src/components/Tinymce.vue index ee54bf0..15002dc 100644 --- a/src/components/Tinymce.vue +++ b/src/components/Tinymce.vue @@ -47,7 +47,7 @@ const customer = (init) => { init.placeholder = props.placeholder // Paste pictures and automatically process base64 - init.urlconverter_callback = (url, node, onSave, name) => { + init.urlconverter_callback = (url, node, _onSave, _name) => { if (node === 'img' && url.startsWith('blob:')) { tinymce.activeEditor && tinymce.activeEditor.uploadImages() } @@ -73,7 +73,7 @@ onMounted(() => { // Listen for text changes and pass them to the outside world watch( () => state.contentValue, - (n) => { + () => { debounce(() => { emit('update:modelValue', state.contentValue) }) @@ -112,7 +112,5 @@ const imgUploadFn = async (blobInfo, success, failure) => { return failure(`HTTP Error: ${response.status}`) } -const { myInit, contentValue, timeout } = toRefs(state) +const { myInit, contentValue } = toRefs(state) - - diff --git a/src/components/form/CheckboxField.vue b/src/components/form/CheckboxField.vue index cf3a19e..d7c8d24 100644 --- a/src/components/form/CheckboxField.vue +++ b/src/components/form/CheckboxField.vue @@ -54,8 +54,8 @@ const onToggle = () => { } &--selected .checkbox-field__box { - background: #999999; - border-color: #999999; + background: #999; + border-color: #999; } &__box { diff --git a/src/components/form/ImageUploader.vue b/src/components/form/ImageUploader.vue index 105ea10..1ccc45d 100644 --- a/src/components/form/ImageUploader.vue +++ b/src/components/form/ImageUploader.vue @@ -122,7 +122,7 @@ onBeforeUnmount(revokePreview) align-items: center; gap: 0.75rem; padding: 1rem; - background: #eeeeee; + background: #eee; border-radius: 1.5rem; min-height: 18rem; justify-content: space-between; diff --git a/src/features/admin/assignments/components/AssignmentsFilters.vue b/src/features/admin/assignments/components/AssignmentsFilters.vue index 18ff6c5..fe363f5 100644 --- a/src/features/admin/assignments/components/AssignmentsFilters.vue +++ b/src/features/admin/assignments/components/AssignmentsFilters.vue @@ -142,9 +142,13 @@ const sessionListFilters = computed(() => ({ courseId: form.value.courseId || undefined, })) const sessionPagination = ref({ page: 1, perPage: 10 }) -const { data: sessionsResponse } = useAdminSessionsListQuery(sessionListFilters, sessionPagination, { - enabled: () => !!form.value.courseId, -}) +const { data: sessionsResponse } = useAdminSessionsListQuery( + sessionListFilters, + sessionPagination, + { + enabled: () => !!form.value.courseId, + } +) const sessionOptions = computed(() => sessionsResponse.value?.data ?? []) const searchTerms = useDebounce((q) => { diff --git a/src/features/admin/assignments/components/modals/AddAssignmentModal.vue b/src/features/admin/assignments/components/modals/AddAssignmentModal.vue index 70de3a2..6770b65 100644 --- a/src/features/admin/assignments/components/modals/AddAssignmentModal.vue +++ b/src/features/admin/assignments/components/modals/AddAssignmentModal.vue @@ -142,14 +142,13 @@ import BaseButton from '@/components/BaseButton.vue' import SvgIcon from '@/components/icons/SvgIcon.vue' import TextField from '@/components/form/TextField.vue' import SelectField from '@/components/form/SelectField.vue' -import LineTitleBlock from '@/components/LineTitleBlock.vue' import FileUploader from '@/components/form/FileUploader.vue' import { objectToFormData } from '@/utils/object-to-formdata' import { useUploadMediaMutation } from '@/services/query/auth' import TextareaField from '@/components/form/TextareaField.vue' import DatePickerField from '@/components/form/DatePickerField.vue' -import { assignmentSchema } from '@/features/admin/assignments/schema' import { useAdminTermsListQuery } from '@/services/query/admin-terms' +import { assignmentSchema } from '@/features/admin/assignments/schema' import { useAdminCoursesListQuery } from '@/services/query/admin-courses' import { useAdminSessionsListQuery } from '@/services/query/admin-sessions' import { diff --git a/src/features/admin/assignments/components/modals/AssignmentDetailsModal.vue b/src/features/admin/assignments/components/modals/AssignmentDetailsModal.vue index 3e109ea..0c071e7 100644 --- a/src/features/admin/assignments/components/modals/AssignmentDetailsModal.vue +++ b/src/features/admin/assignments/components/modals/AssignmentDetailsModal.vue @@ -41,7 +41,7 @@