Files

1262 lines
117 KiB
Markdown
Raw Permalink Normal View History

# Inventory — apps/admin-panel (target admin)
All paths relative to `apps/admin-panel/`. Read from source; `node_modules`, `dist`, `branchDist` ignored.
Root = `E:\gold bank\gold-banking-monorepo\apps\admin-panel`.
---
## 1. Stack & versions
Source: `package.json:1-37`.
| Concern | Package | Version | Line |
|---|---|---|---|
| Framework | `vue` | `^3.5.0` | `package.json:23` |
| UI library | `ant-design-vue` | `^4.2.6` | `package.json:17` |
| UI icons | `@ant-design/icons-vue` | `^7.0.1` | `package.json:14` |
| Router | `vue-router` | `^4.4.0` | `package.json:24` |
| Store | `pinia` | `^2.2.0` | `package.json:22` |
| HTTP client | `axios` | `^1.7.7` | `package.json:19` |
| Date library | `moment-jalaali` | `^0.10.0` | `package.json:21` |
| Charts | `apexcharts` `^7.4.0` + `vue3-apexcharts` `^1.11.1` | — | `package.json:18,25` |
| Utility | `lodash` `^4.17.21`, `@vueuse/core` `^9.13.0` | — | `package.json:16,20` |
| Shared types | `@gold-bank/shared-types` (`workspace:*`) | — | `package.json:15` |
| Build | `vite` `^5.4.0`, `@vitejs/plugin-vue` `^5.1.0`, `vue-tsc` `^2.1.0`, `typescript` `^5.5.0`, `less` `^4.1.2` | — | `package.json:28-35` |
Scripts (`package.json:5-12`): `dev` (vite), `dev:branch` (`vite --mode branch`), `build` (`vue-tsc -b && vite build`), `build:branch` (`--mode branch`), `preview`, `lint`.
Bootstrap `src/main.ts:15-22`: `createApp(App)` → `createPinia()` → `Antd` → `router` → `VueApexCharts` → mount `#app`. Styles `./assets/styles/main.less` (`src/main.ts:13`). Vue-2 legacy plugins (Vuesax, VeeValidate, vue2-hammer, vue-clipboard2, ACL, i18n) intentionally dropped (`src/main.ts:1-5`).
`index.html:2` — `<html lang="fa" dir="rtl">`, title `منتزر — پنل مدیریت` (`index.html:7`), preloader `#loading-bg` removed by `router.afterEach` (`index.html:12`, `src/router/index.ts:63-68`).
`src/env.d.ts:12-17` declares the global `apexchart` component for vue-tsc.
---
## 2. HTTP layer
### 2.1 Client — `src/api/client.ts`
| Aspect | Value | Line |
|---|---|---|
| Base URL | `import.meta.env.VITE_API_BASE_URL ?? '/api/'` | `src/api/client.ts:11` |
| Default headers | `Content-Type: application/json`, `Accept: application/json` | `src/api/client.ts:13-16` |
| Boot token | reads `localStorage[tokenName]` and seeds `Authorization: Bearer <t>` | `src/api/client.ts:18-21` |
| Instance | `axios.create({ baseURL, headers })` | `src/api/client.ts:23-26` |
**Request interceptor** (`src/api/client.ts:34-58`):
1. Rewrites `config.url` through `mapEndpoint()` (`:36-38`).
2. Re-reads token each request; sets `Authorization: Bearer <token>` when present, otherwise **deletes** the header (legacy sent literal `"Bearer null"` — fixed) (`:39-44`).
3. If `config.data instanceof FormData`, deletes `Content-Type` so the browser sets the multipart boundary (`:52-54`) — required for `file/create` uploads.
**Response interceptor** (`src/api/client.ts:68-82`): NOT in legacy. On HTTP **401**, if not already on `/auth/login` and not already redirecting, removes the token and does `window.location.assign(`${import.meta.env.BASE_URL}auth/login`)`. Guarded by a module `redirecting` flag so it fires once. **There is no refresh-token flow** — no `/refresh` call, no retry queue.
**Error handling / toasts**: none centrally. Every view does its own `try/catch` + `message.error(e?.response?.data?.message ?? '<farsi fallback>')`. Shared delete errors go through `Modal.error` in `src/composables/useDeleteHandler.ts:43-46`.
Token storage key is mode-dependent: `tokenName = isBranch() ? 'branch_token' : 'token'`, `userName = isBranch() ? 'branch_user' : 'user'` (`src/utils/index.ts:18-19`).
### 2.2 Endpoint mapping layer — `src/api/endpoints.ts`
Legacy path → new REST path rewrite, applied inside the request interceptor. `normalize()` strips leading slashes (`:20-22`); `mapEndpoint()` checks `EXACT` first, then `PATTERNS` in order, else passes through; absolute `http(s)://` URLs bypass entirely (`:251-270`). `isMapped()` is a dev diagnostic (`:273-276`).
#### EXACT map (`src/api/endpoints.ts:29-168`)
| Legacy path | New path | Line |
|---|---|---|
| `file/create` | `admin/files/create` | `:33` |
| `admin-login` | `admin/auth/login` | `:36` |
| `users/show` | `admin/auth/me` | `:37` |
| `single-order` | `admin/orders/list` | `:41` |
| `single/order-sts/edit` | `admin/orders/status` | `:42` |
| `major/order/table` | `admin/orders/wholesale/list` | `:43` |
| `order-tamin` | `admin/orders/tamin/list` | `:44` |
| `order-sts/edit` | `admin/orders/status` | `:48` |
| `delete-order` | `admin/orders/delete` | `:49` |
| `get-custom-orders` | `admin/pre-orders/list` | `:52` |
| `customer` | `admin/customers-omde/list` | `:56` |
| `banker` | `admin/customers-omde/banker/list` | `:57` |
| `shopkeeper` | `admin/customers-omde/shopkeeper/list` | `:58` |
| `major/verify/table` | `admin/customers-omde/non-verified/list` | `:59` |
| `major/archive/table` | `admin/customers-omde/archived/list` | `:60` |
| `reagent` | `admin/customers-omde/reagent/list` | `:61` |
| `customer/toggle-active` | `admin/customers-omde/toggle-active` | `:62` |
| `customer/toggle-sts` | `admin/customers-omde/toggle-sts` | `:63` |
| `major/verify` | `admin/customers-omde/verify` | `:64` |
| `single-customer` | `admin/customers-omde/single/list` | `:67` |
| `single` | `admin/customers-omde/single` (sets `kind=RETAIL`) | `:75` |
| `customer/create` | `admin/customers-omde/create` | `:76` |
| `query/reagent` | `admin/customers-omde/query/reagent` | `:77` |
| `wallet/charge` | `admin/customers-omde/wallet/charge` | `:78` |
| `comment/table` | `admin/comments/list` | `:79` |
| `comment/sts` | `admin/comments/sts` | `:80` |
| `comment` | `admin/comments` | `:81` |
| `order` | `admin/orders/create` | `:84` |
| `order/type` | `admin/orders/type-options` | `:85` |
| `add/variant` | `admin/products/variants/create` | `:86` |
| `product/variant` | `admin/products/by-ids` | `:87` |
| `products` | `admin/products/list` | `:90` |
| `product/create` | `admin/products/create` | `:91` |
| `get/variants` | `admin/products/variants/list` | `:92` |
| `change/percent/count` | `admin/products/change-percent/count` | `:93` |
| `change/all` | `admin/products/change-percent/apply` | `:94` |
| `cash/table` | `admin/documents/list` | `:97` |
| `cash` | `admin/documents` | `:98` |
| `query/customer` | `admin/documents/query/customer` | `:99` |
| `wallet/table` | `admin/documents/wallet/list` | `:100` |
| `wallet/config` | `admin/documents/wallet/config` | `:101` |
| `wallet/birthday/config` | `admin/documents/wallet/birthday/config` | `:102` |
| `product/variant/promotion` | `admin/promotions` | `:105` |
| `major/promotion` | `admin/promotions/list` | `:106` |
| `autopromotion` | `admin/promotions/auto` | `:107` |
| `ignore` | `admin/promotions/exclusions` | `:108` |
| `users` | `admin/users` | `:113` |
| `users/list` | `admin/users/list` | `:114` |
| `const/__list__ut` | `admin/users/types` | `:115` |
| `const` | `admin/users/types` | `:116` |
| `roles/create` | `admin/users/roles` | `:117` |
| `branch/product` | `admin/invoices` | `:120` |
| `branch/product/table` | `admin/invoices/list` | `:121` |
| `branch/list` | `admin/invoices/branch-list` | `:122` |
| `branch` | `admin/branches` | `:125` |
| `branch/table` | `admin/branches/list` | `:126` |
| `workshop` | `admin/workshops` | `:127` |
| `user/list` | `admin/branches/user-list` | `:128` |
| `cities` | `admin/misc/cities` | `:131` |
| `sms` | `admin/misc/sms` | `:132` |
| `sms/table` | `admin/misc/sms/list` | `:133` |
| `notify` | `admin/misc/notify` | `:134` |
| `notify/table` | `admin/misc/notify/list` | `:135` |
| `notify/customer` | `admin/misc/notify/customer` | `:136` |
| `ddf` | `admin/misc/newsletter/list` | `:138` |
| `fashion/table` | `admin/misc/newsletter/list` | `:139` |
| `log/list/customer` | `admin/misc/log/customers` | `:140` |
| `dashboard/link` | `admin/misc/dashboard-links` | `:141` |
| `get-analytics` | `admin/dashboard/omde` | `:144` |
| `get-analytics-tak` | `admin/dashboard/tak` | `:145` |
| `dashboard/overview` | `admin/dashboard/overview` | `:146` |
| `brand/create` | `admin/misc/brands` | `:147` |
| `ciny/create` | `admin/categories/collections` | `:149` |
| `cities/list` | `admin/cities/list` | `:152` |
| `report/order-item` | `admin/dashboard/report/order-item` | `:155` |
| `report/product` | `admin/dashboard/report/product` | `:156` |
| `workshop/table` | `admin/workshops/list` | `:157` |
| `brand` | `admin/products/brands` | `:158` |
| `category` | `admin/categories/list` | `:165` |
| `category/` | `admin/categories` | `:166` |
| `ciny` | `admin/categories/collections/list` | `:167` |
#### PATTERN map (`src/api/endpoints.ts:173-248`) — first match wins
| Pattern | Rewrite | Line |
|---|---|---|
| `cities/get-percent-aux/(.+)` | `admin/misc/cities/percent-aux/$1` | `:185` |
| `cities/get-percent/(.+)` | `admin/misc/cities/percent/$1` | `:186` |
| `cities/(.+)` | `admin/misc/cities/$1` | `:187` |
| `const/(.+)` | `admin/misc/const/$1` | `:190` |
| `brand/(.+)` | `admin/misc/brands/$1` | `:193` |
| `log/ago/(.+)` | `admin/misc/log/customer/$1` | `:196` |
| `notify/read/(.+)` | `admin/misc/notify/read/$1` | `:199` |
| `notify/(.+)` | `admin/misc/notify/$1` | `:200` |
| `single/(.+)` | `admin/customers-omde/$1` | `:203` |
| `custom-order/(.+)` | `admin/pre-orders/$1` | `:206` |
| `get-custom-order/(.+)` | `admin/pre-orders/$1` | `:207` |
| `major/archive/(.+)` | `admin/customers-omde/archive/$1` | `:209` |
| `major/dashboard/(.+)` | `admin/customers-omde/dashboard/$1` | `:210` |
| `comment/(.+)` | `admin/comments/$1` | `:213` |
| `customer/(.+)` | `admin/customers-omde/$1` | `:215` |
| `delete/product/(.+)` | `admin/products/delete/$1` | `:218` |
| `product/show/(.+)` | `admin/products/$1` | `:219` |
| `product/update/(.+)` | `admin/products/$1` | `:220` |
| `product/(.+)/variant` | `admin/products/$1/variants` | `:221` |
| `category/(.+)` | `admin/categories/$1` | `:223` |
| `ciny/(.+)` | `admin/categories/collections/$1` | `:224` |
| `cash/confirem/(.+)` | `admin/documents/$1/confirm` (client keeps legacy misspelling) | `:227` |
| `cash/(.+)` | `admin/documents/$1` | `:228` |
| `product/variant/promotion/(.+)` | `admin/promotions/$1` | `:231` |
| `major/promotion/(.+)` | `admin/promotions/$1` | `:232` |
| `ignore/(.+)` | `admin/promotions/exclusions/$1` | `:233` |
| `roles/(\d+)/(.+)` | `admin/users/types/$1/roles/$2` | `:236` |
| `roles/(.+)` | `admin/users/types/$1/roles` | `:237` |
| `users/(.+)` | `admin/users/$1` | `:238` |
| `branch/product/(.+)` | `admin/invoices/$1` | `:243` |
| `branch/active/(.+)` | `admin/branches/active/$1` | `:244` |
| `workshop/active/(.+)` | `admin/workshops/active/$1` | `:245` |
| `branch/(.+)` | `admin/branches/$1` | `:246` |
| `workshop/(.+)` | `admin/workshops/$1` | `:247` |
### 2.3 Every API call site (method + URL as written; resolve via §2.2)
| File:line | Method | URL (as written) |
|---|---|---|
| `src/components/upload/ImageUploadInput.vue:97` | POST | `file/create` (FormData) |
| `src/components/x-table/XTable.vue:288` | POST | `props.model.url` (per table model, §7/§6) |
| `src/composables/useDeleteHandler.ts:34,36` | POST / DELETE | caller-supplied url |
| `src/router/index.ts:87` | GET | `users/show` |
| `src/stores/auth.ts:58` | POST | `admin-login` |
| `src/stores/cart.ts:102` | POST | `product/variant` `{products_id}` |
| `src/stores/users.ts:94` | POST | `cities/list` |
| `src/views/Dashboard.vue:301` | GET | `dashboard/overview` |
| `src/views/apps/category/Add.vue:215` | PUT | `category/{id}` |
| `src/views/apps/category/Add.vue:217` | POST | `category/` |
| `src/views/apps/category/Add.vue:235` | GET | `admin/categories/tree` |
| `src/views/apps/category/Add.vue:243` | GET | `admin/categories/{id}` |
| `src/views/apps/customer/ReagentCustomer.vue:154` | PUT | `users/{id}` |
| `src/views/apps/customer/ReagentCustomer.vue:156` | POST | `users` |
| `src/views/apps/customer/ReagentCustomer.vue:171` | GET | `admin/users/{id}` |
| `src/views/apps/customer/bulkCustomer.vue:400` | PUT | `customer/{id}` |
| `src/views/apps/customer/bulkCustomer.vue:402` | POST | `customer/create` |
| `src/views/apps/customer/bulkCustomer.vue:422` | GET | `query/reagent` |
| `src/views/apps/customer/bulkCustomer.vue:428` | POST | `cities/list` |
| `src/views/apps/customer/bulkCustomer.vue:435` | GET | `customer/{id}` |
| `src/views/apps/customer/singleCustomer.vue:97` | PUT | `single/{id}` |
| `src/views/apps/customer/singleCustomer.vue:99` | POST | `single` |
| `src/views/apps/customer/singleCustomer.vue:112` | POST | `cities/list` |
| `src/views/apps/customer/singleCustomer.vue:120` | GET | `customer/{id}` |
| `src/views/apps/product/AddProduct.vue:437` | PUT | `product/update/{id}` |
| `src/views/apps/product/AddProduct.vue:439` | POST | `product/create` |
| `src/views/apps/product/AddProduct.vue:453` | GET | `brand` |
| `src/views/apps/product/AddProduct.vue:454` | POST | `category` |
| `src/views/apps/product/AddProduct.vue:455` | GET | `admin/products/collections` |
| `src/views/apps/product/AddProduct.vue:472` | GET | `product/show/{id}` |
| `src/views/apps/product/ChangePercent.vue:87` | POST | `change/percent/count` |
| `src/views/apps/product/ChangePercent.vue:104` | POST | `change/all` |
| `src/views/apps/product/ChangePercent.vue:121` | GET/POST | `brand`, `category` |
| `src/views/apps/product/ProductList.vue:131` | GET | `admin/products/{id}` |
| `src/views/apps/product/ProductList.vue:146` | POST | `delete/product/{id}` (via delete handler) |
| `src/views/branch/AddBranches.vue:139` | PUT | `branch/{id}` |
| `src/views/branch/AddBranches.vue:141` | POST | `branch` |
| `src/views/branch/AddBranches.vue:162` | GET | `user/list` |
| `src/views/branch/AddBranches.vue:169` | GET | `branch/{id}` |
| `src/views/branch/FacilityList.vue:104` | GET | `{activePath}/{id}` (`branch/active` \| `workshop/active`) |
| `src/views/branch/buyList.vue:69` | GET | `branch/product/{id}` |
| `src/views/branch/dashboard.vue:75` | GET | `get-analytics-tak` |
| `src/views/branch/dashboard.vue:76` | POST | `products` `{page:1,limit:5}` |
| `src/views/branch/order/create.vue:213` | POST | `order` |
| `src/views/branch/order/create.vue:233` | POST | `single-customer` `{page:1,limit:500}` |
| `src/views/city/cityList.vue:92` | GET | `cities/get-percent/{id}` |
| `src/views/city/cityList.vue:93` | GET | `cities/get-percent-aux/{id}` |
| `src/views/city/cityList.vue:106` | PUT | `cities/{id}` |
| `src/views/city/createCity.vue:85` | POST | `cities` |
| `src/views/customer/Comments/Comments.vue:198` | POST | `comment/sts` `{id, sts}` |
| `src/views/customer/Comments/Comments.vue:215` | DELETE | `comment/{id}` |
| `src/views/customer/Comments/Comments.vue:225` | POST | `comment` |
| `src/views/customer/CustomerDetail.vue:174` | GET | `customer/{id}` |
| `src/views/customer/CustomerDetail.vue:175` | GET | `major/dashboard/{id}` |
| `src/views/customer/CustomerDetail.vue:176` | GET | `admin/customers-omde/{id}/orders` |
| `src/views/customer/CustomerDetail.vue:177` | GET | `admin/customers-omde/{id}/documents` |
| `src/views/customer/CustomerDetail.vue:191` | GET | `log/ago/{id}` |
| `src/views/customer/omde/OmdeCustomerList.vue:352` | POST | `major/verify` |
| `src/views/customer/omde/OmdeCustomerList.vue:391` | POST | `customer/toggle-active` |
| `src/views/customer/omde/OmdeCustomerList.vue:403` | POST | `customer/toggle-sts` |
| `src/views/customer/omde/OmdeCustomerList.vue:413` | GET | `major/archive/{id}` |
| `src/views/customer/reagentList/AgentDetail.vue:108` | GET | `admin/customers-omde/reagent/{id}` |
| `src/views/customer/reagentList/ReagentList.vue:109` | PUT | `users/{id}` `{is_active}` |
| `src/views/customer/single-customer/SingleCustomer.vue:99` | POST | `wallet/charge` |
| `src/views/finical/addFinDoc.vue:305` | PUT | `cash/{id}` |
| `src/views/finical/addFinDoc.vue:307` | POST | `cash` |
| `src/views/finical/addFinDoc.vue:325` | GET | `query/customer` |
| `src/views/finical/addFinDoc.vue:333` | GET | `cash/{id}` |
| `src/views/finical/list.vue:111` | POST | `cash/confirem/{id}` |
| `src/views/invoice/AddInvoice.vue:231` | PUT | `branch/product/{id}` |
| `src/views/invoice/AddInvoice.vue:233` | POST | `branch/product` |
| `src/views/invoice/AddInvoice.vue:253` | GET | `branch/list` |
| `src/views/invoice/AddInvoice.vue:261` | GET | `branch/product/{id}` |
| `src/views/invoice/ListInvoice.vue:113` | GET | `branch/product/{id}` |
| `src/views/manage/Const.vue:217` | GET | `brand` |
| `src/views/manage/Const.vue:224` | POST | `brand/create` |
| `src/views/manage/Const.vue:241` | PUT | `brand/{id}` |
| `src/views/manage/Const.vue:275` | PUT | `const/__list__tags` `{val}` |
| `src/views/manage/Const.vue:290` | PUT | `const/slider` `{val}` |
| `src/views/manage/Const.vue:299` | POST | `ciny` `{page:1,limit:200}` |
| `src/views/manage/Const.vue:307` | PUT | `ciny/{id}` |
| `src/views/manage/Const.vue:309` | POST | `ciny/create` |
| `src/views/manage/Const.vue:333` | DELETE | `ciny/{id}` |
| `src/views/manage/Const.vue:346-347` | GET | `const/__list__tags`, `const/slider` |
| `src/views/manage/report.vue:24` | GET | `dashboard/link` |
| `src/views/manage/wallet.vue:101` | POST | `wallet/config` |
| `src/views/manage/wallet.vue:122` | POST | `wallet/birthday/config` |
| `src/views/manage/wallet.vue:137-138` | GET | `wallet/config`, `wallet/birthday/config` |
| `src/views/notify/create.vue:47` | POST | `notify` |
| `src/views/notify/user.vue:73` | GET | `notify/{id}` |
| `src/views/notify/user.vue:81` | GET | `notify/read/{id}` |
| `src/views/offersIgnore/offersIgnore-add-edit.vue:166` | POST | `ignore` |
| `src/views/offersIgnore/offersIgnore-add-edit.vue:184` | DELETE | `ignore/{type}` |
| `src/views/offersIgnore/offersIgnore-add-edit.vue:198` | GET | `ignore` |
| `src/views/order/List.vue:197` | GET | `admin/orders/{id}` |
| `src/views/order/List.vue:203,215` | POST | `single/order-sts/edit` |
| `src/views/order/MakeOrder.vue:168` | POST | `workshop/table` `{page:1,limit:100}` |
| `src/views/order/MakeOrder.vue:177` | GET | `admin/pre-orders/{id}` |
| `src/views/order/MakeOrder.vue:193` | PUT | `custom-order/{id}` |
| `src/views/order/OmdeList.vue:171` | GET | `admin/orders/{id}` |
| `src/views/order/OmdeList.vue:187,198` | POST | `order-sts/edit` |
| `src/views/order/TaminList.vue:153` | GET | `admin/orders/{id}` |
| `src/views/order/TaminList.vue:169,184` | POST | `order-sts/edit` |
| `src/views/order/create.vue:327` | POST | `add/variant` |
| `src/views/order/create.vue:373` | POST | `order` |
| `src/views/order/create.vue:394` | GET | `order/type` |
| `src/views/order/create.vue:395` | POST | `single-customer` `{page:1,limit:500}` |
| `src/views/promotion/PromotionForm.vue:335` | PUT | `product/variant/promotion/{id}` |
| `src/views/promotion/PromotionForm.vue:337` | POST | `product/variant/promotion` |
| `src/views/promotion/PromotionForm.vue:356-358` | POST/GET | `products`, `category`, `brand` (limit 500) |
| `src/views/promotion/PromotionForm.vue:369` | GET | `query/customer` |
| `src/views/promotion/PromotionForm.vue:378` | GET | `admin/promotions/{id}` |
| `src/views/promotion/autopromotion.vue:55` | POST | `autopromotion` |
| `src/views/promotion/autopromotion.vue:69` | GET | `autopromotion` |
| `src/views/promotion/promotion_list.vue:129` | DELETE | `major/promotion/{id}` |
| `src/views/reg.vue:92` | POST | `customer/create` |
| `src/views/report/order_item.vue:76` | GET | `report/order-item` `{params: filters}` |
| `src/views/report/product.vue:60` | GET | `report/product` `{params: filters}` |
| `src/views/sms/create.vue:94` | POST | `sms` |
| `src/views/statics/index.vue:71` | GET | `log/ago/{customer}` |
| `src/views/statics/index.vue:82` | GET | `log/list/customer` |
| `src/views/user/ListUser.vue:90` | GET | `const/__list__ut` |
| `src/views/user/Permission.vue:118` | GET | `const/__list__ut` |
| `src/views/user/Permission.vue:136` | GET | `roles/{id}` |
| `src/views/user/Permission.vue:160` | POST | `const` (create user type) |
| `src/views/user/Permission.vue:167` | POST | `roles/create` `{ut, roles}` |
| `src/views/user/UserCreate.vue:220` | PUT | `users/{id}` |
| `src/views/user/UserCreate.vue:222` | POST | `users` |
| `src/views/user/UserCreate.vue:241-242` | GET | `const/__list__ut`, `admin/users/role-options` |
| `src/views/user/UserCreate.vue:252` | GET | `admin/users/{id}` |
| `src/views/user/myData.vue:37` | GET | `admin/users/me` |
| `src/views/workshop/AddWorkshop.vue:151` | PUT | `workshop/{id}` |
| `src/views/workshop/AddWorkshop.vue:153` | POST | `workshop` |
| `src/views/workshop/AddWorkshop.vue:172` | GET | `workshop/{id}` |
---
## 3. Router — every route
### 3.1 Structure — `src/router/index.ts`
`createWebHistory(import.meta.env.BASE_URL)` (`:10`); `scrollBehavior` returns `{left:0,top:0}` (`:12-14`).
Two top-level shells (`:17-53`):
- `path: ''` → `layouts/main/Main.vue`, children = `isBranch() ? branchRoute : adminRoute` (`:18-21`)
- `path: ''` → `layouts/full-page/FullPage.vue`, children = `fullPage*Route` + 4 error pages (`:23-53`)
- catch-all `/:pathMatch(.*)*` → redirect `/pages/error-404` (`:55-58`)
### 3.2 Admin routes (`src/router/admin.ts`) — main layout (`layouts/main/Main.vue`)
`meta.layout` does not exist as a key; layout is determined by the parent shell. Meta keys actually used: `rule`, `role`, `parent`, `no_scroll`, `pageTitle`, `breadcrumb`.
| path | name | component file | meta (rule / role / parent / no_scroll / pageTitle) | line |
|---|---|---|---|---|
| `/` | `dashboard` | — (redirect `/dashboard/ecommerce`) | — | `:27-30` |
| `/dashboard/analytics` | `dashboard-analytics` | `views/Dashboard.vue` | editor / `dashboard.view` | `:36-43` |
| `/my-data` | `my-data` | `views/user/myData.vue` | editor / (role commented out) | `:45-52` |
| `/dashboard/ecommerce` | `dashboard-ecommerce` | `views/Dashboard.vue` | **admin** / `dashboard.view` | `:54-61` |
| `/config/manage/const` | — | `views/manage/Const.vue` | editor / `config.statics` / manage / true / `مدیریت ثابت ها` | `:63-82` |
| `/config/manage/wallet` | — | `views/manage/wallet.vue` | editor / `config.wallet` / manage / true / `مدیریت کیف پول` | `:84-103` |
| `/config/manage/report` | — | `views/manage/report.vue` | editor / `config.report` / manage / true / `گزارش` | `:105-124` |
| `/apps/product/` | `product` | — (redirect `/apps/product/list`) | — | `:127-130` |
| `/product/change/percent` | — | `views/apps/product/ChangePercent.vue` | editor / `product.percent` / product / true | `:132-140` |
| `/apps/product/add` | — | `views/apps/product/AddProduct.vue` | editor / `product.create` / product / true | `:142-150` |
| `/apps/product/edit/:id/:variant_id?` | `product-edit` | `views/apps/product/EditProduct.vue` | editor / `product.list.edit` / product / true | `:152-161` |
| `/apps/product/list` | `product-list` | `views/apps/product/ProductList.vue` | editor / `product.list` / product / true | `:163-172` |
| `/category` | `category` | — (redirect `/category/list`) | — | `:175-178` |
| `/category/add/:id?` | `category-add` | `views/apps/category/Add.vue` | editor / `collection.create` / category / true | `:180-189` |
| `/category/edit/:id` | `category-edit` | `views/apps/category/Edit.vue` | editor / `collection.list.edit` / category / true | `:191-200` |
| `/category/list` | `category-list` | `views/apps/category/List.vue` | editor / `collection.list` / category / true | `:202-211` |
| `/add/findoc` | — | `views/finical/addFinDoc.vue` | editor / `document.create` | `:213-221` |
| `/edit/findoc/:id` | — | `views/finical/addFinDoc.vue` | editor / `document.list.edit` | `:222-230` |
| `/order` | `order` | — (redirect `/order/list`) | — | `:232-236` |
| `/order/list` | — | `views/order/List.vue` | editor / `order.list` / order / true | `:238-246` |
| `/order/omde-list` | — | `views/order/OmdeList.vue` | editor / `order.omde` / order / true | `:248-256` |
| `/order/make` | — | `views/order/MakeOrder.vue` | editor / `order.makeOrder` / order / true | `:258-266` |
| `/order/create` | — | `views/order/create.vue` | editor / `order.create` / order / true | `:268-276` |
| `/order/tamin-list` | — | `views/order/TaminList.vue` | editor / `order.tamin` / order / true | `:278-286` |
| `/major/promotion/list` | — | `views/promotion/promotion_list.vue` | editor / `offers.list` / true | `:288-296` |
| `/major/promotion/add` | — | `views/promotion/promotion_add_edit.vue` | editor / `offers.create` / true | `:298-306` |
| `/major/promotion/edit/:id` | — | `views/promotion/promotion_add_edit.vue` | editor / `offers.list.edit` / true | `:308-316` |
| `/major/promotion/tak/add` | — | `views/promotion/promotion_tak_add_edit.vue` | editor / `offers.create.tak` / true | `:318-326` |
| `/major/promotion/tak/edit/:id` | — | `views/promotion/promotion_tak_add_edit.vue` | editor / `offers.list.tak.edit` / true | `:328-336` |
| `/major/promotion/autopromotion` | — | `views/promotion/autopromotion.vue` | editor / `offers.autopromotion` / true | `:338-345` |
| `/major/offersIgnore` | — | `views/offersIgnore/offersIgnore-add-edit.vue` | editor / `offersIgnore` | `:347-354` |
| `/comments` | `comments` | `views/customer/Comments/Comments.vue` | editor / `comment` / true / `دیدگاه ها` | `:356-377` |
| `/sms/create` | `create_sms` | `views/sms/create.vue` | editor / `sms.create` / true / `ارسال پیامک` | `:379-398` |
| `/notify` | `notify` | `views/notify/user.vue` | editor / `notify.me` / true / `لیست نوتیفیکیشن های ذریافتی` | `:400-419` |
| `/notify/create` | `create_notify` | `views/notify/create.vue` | editor / `notify.create` / true / `ارسال نوتیفیکیشن` | `:421-440` |
| `/wallet` | `wallet` | `views/wallet/wallet.vue` | editor / `wallet` / true / `لیست کیف پول های شارژ شده` | `:442-461` |
| `/reagent/edit/:id` | `reagent-edit` | `views/apps/customer/EditReagent.vue` | editor / `persons.list.sellers.edit` / product / true | `:463-473` |
| `/reagent/add` | `reagent-add` | `views/apps/customer/EditReagent.vue` | editor / `persons.create` / product / true | `:475-485` |
| `/reagent/view/:id` | `reagent-view` | `views/customer/reagentList/AgentDetail.vue` | editor / `persons.list.sellers.view` / product / true | `:487-496` |
| `/bulkCustomer/edit/:id` | `bulkCustomer-edit` | `views/apps/customer/EditBulkCustomer.vue` | editor / `persons.list.client.omde.edit` / product / true | `:498-507` |
| `/bulkCustomer/view/:id` | `bulkCustomer-view` | `views/customer/omde/ViewOmdeCustomer.vue` | editor / `persons.list.client.omde.view` / product / true | `:509-518` |
| `/singleCustomer/edit/:id` | `singleCustomer-edit` | `views/apps/customer/EditSingleCustomer.vue` | editor / `persons.list.client.single.edit` / product / true | `:520-529` |
| `/singleCustomer/view/:id` | `singleCustomer-view` | `views/customer/single-customer/ViewSingleCustomer.vue` | editor / `persons.list.client.single.view` / product / true | `:531-540` |
| `/customer/omde-list` | `omde-list-customer` | `views/customer/omde/OmdeList.vue` | editor / `persons.list.client.omde` / `مشتریان عمده` | `:542-560` |
| `/customer/omde-non-verified-list` | `omde-non-verified-list` | `views/customer/omde/OmdeNonVerifiedList.vue` | editor / `persons.list.client.omde` / `مشتریان در انتظار` | `:562-580` |
| `/customer/banker-list` | `banker-list` | `views/customer/omde/BankerList.vue` | editor / `persons.list.client.omde` / `لیست بنکداران` | `:582-600` |
| `/customer/shopkeeper-list` | `shopkeeper-list` | `views/customer/omde/ShopkeeperList.vue` | editor / `persons.list.client.omde` / `لیست مغازه داران حضوری` | `:602-620` |
| `/customer/omde-archived-list` | `omde-archived-list` | `views/customer/omde/OmdeArchivedList.vue` | editor / `persons.list.client.omde` / `مشتریان بایگانی` | `:622-640` |
| `/customer/reagent-list` | `raegentList` | `views/customer/reagentList/ReagentList.vue` | editor / `persons.list.sellers` / `کارشناسان فروش` | `:642-660` |
| `/customer/add` | `add-customer` | `views/apps/customer/AddCustomer.vue` | editor / `persons.create` / `افزودن مشتری` | `:662-681` |
| `/customer/list` | `list-customer` | `views/customer/single-customer/SingleCustomer.vue` | editor / `persons.list.client.single` / `لیست مشتریان` | `:683-698` |
| `/report/order_item` | `report_order_item` | `views/report/order_item.vue` | editor / `report.order.item` | `:700-712` |
| `/report/product` | `report_product` | `views/report/product.vue` | editor / `report.product` | `:714-721` |
| `/finical/list` | `finical_list` | `views/finical/list.vue` | editor / `document.list` | `:723-736` |
| `/user/list` | — | `views/user/ListUser.vue` | editor / `user.list` / user / true / `لیست مدیران` | `:738-757` |
| `/user/create` | — | `views/user/UserCreate.vue` | editor / `user.create` / user / true / `افزودن کاربر` | `:759-778` |
| `/user/edit/:id` | — | `views/user/UserEdit.vue` | editor / `user.list.edit` / user / true / `ویرایش نوع کاربر` | `:780-799` |
| `/permission` | — | `views/user/Permission.vue` | editor / `user.permission` / permission / true / `دسترسی ها` | `:802-821` |
| `/city/create` | — | `views/city/createCity.vue` | editor / `cities.create` / city / true / `افزودن شهر` | `:823-842` |
| `/city/list` | — | `views/city/cityList.vue` | editor / `cities.list` / city / true / `لیست شهر ها` | `:844-863` |
| `/statics` | `statics` | `views/statics/index.vue` | editor / `statics` / true / `آمار` | `:866-885` |
| `/newsletters` | `newsletters` | `views/newsletters/newsletters.vue` | editor / **role commented out** / true / `خبرنامه` | `:887-906` |
| `/branch/add` | `add-branch` | `views/branch/AddBranches.vue` | editor / `branch.create` / `افزودن شعب` | `:908-927` |
| `/branch/edit/:id` | `edit-branch` | `views/branch/EditBranches.vue` | editor / `branch.create` / `ویرایش شعب` | `:929-937` |
| `/branch/list` | `list-branch` | `views/branch/ListBranches.vue` | editor / `branch.list` / `لیست شعب` | `:939-958` |
| `/workshop/add` | `add-workshop` | `views/workshop/AddWorkshop.vue` | editor / `workshop.create` / `افزودن کارگاه` | `:961-980` |
| `/workshop/edit/:id` | `edit-workshop` | `views/workshop/EditWorkshop.vue` | editor / `workshop.create` / `ویرایش کارگاه` | `:982-990` |
| `/workshop/list` | `list-workshop` | `views/workshop/ListWorkshop.vue` | editor / `workshop.list` / `لیست کارگاه ها` | `:992-1011` |
| `/invoice/add` | `add-invoice` | `views/invoice/AddInvoice.vue` | editor / `invoice.create` / `افزودن فاکتور` | `:1014-1033` |
| `/invoice/edit/:id` | `edit-invoice` | `views/invoice/EditInvoice.vue` | editor / `invoice.create` / `ویرایش فاکتورها` | `:1035-1043` |
| `/invoice/list` | `list-invoice` | `views/invoice/ListInvoice.vue` | editor / `invoice.list` / `لیست فاکتورها` | `:1045-1064` |
**Admin full-page** (`src/router/admin.ts:1068-1085`, shell `layouts/full-page/FullPage.vue`):
| path | name | component | meta | line |
|---|---|---|---|---|
| `/auth/login` | `page-login` | `views/auth/Login.vue` | rule editor | `:1070-1076` |
| `/reg` | `reg` | `views/reg.vue` | rule editor | `:1078-1084` |
**Shared full-page pages** (`src/router/index.ts:28-51`):
| path | name | component | meta | line |
|---|---|---|---|---|
| `/pages/error-404` | `page-error-404` | `views/pages/Error404.vue` | rule editor | `:28-33` |
| `/pages/error-500` | `page-error-500` | `views/pages/Error500.vue` | rule editor | `:34-39` |
| `/pages/not-authorized` | `page-not-authorized` | `views/pages/NotAuthorized.vue` | rule editor | `:40-45` |
| `/pages/maintenance` | `page-maintenance` | `views/pages/Maintenance.vue` | rule editor | `:46-51` |
### 3.3 Branch routes (`src/router/branch.ts`, active only when `MODE === 'branch'`)
| path | name | component file | meta | line |
|---|---|---|---|---|
| `/` | `main` | — (redirect `/dashboard`) | — | `:15-18` |
| `/dashboard` | `dashboard` | `views/branch/dashboard.vue` | **admin** / `dashboard.view` | `:20-29` |
| `/products` | `product-list` | `views/apps/product/ProductList.vue` | editor / `product.list` / product / true | `:31-40` |
| `/order` | `order` | — (redirect `/order/list`) | — | `:42-45` |
| `/order/list` | — | `views/order/List.vue` | editor / `order.list` / order / true | `:47-55` |
| `/buy/order/list` | — | `views/branch/buyList.vue` | editor / `order.list` / order / true | `:57-65` |
| `/buy/order/list` (**duplicate path**) | — | `views/order/List.vue` | editor / `buy.order.list` / true | `:67-74` |
| `/order/create` | — | `views/branch/order/create.vue` | editor / `order.create` / order / true | `:76-84` |
| `/singleCustomer/edit/:id` | `singleCustomer-edit` | `views/apps/customer/EditSingleCustomer.vue` | editor / `persons.list.client.single.edit` / product / true | `:86-95` |
| `/singleCustomer/view/:id` | `singleCustomer-view` | `views/customer/single-customer/ViewSingleCustomer.vue` | editor / `persons.list.client.single.view` / product / true | `:97-106` |
| `/singleCustomer/edit/:id` (**duplicate**) | `singleCustomer-edit` | `views/apps/customer/EditSingleCustomer.vue` | editor / `persons.list.client.single.edit` / product / true | `:108-117` |
| `/customer/add` | `add-customer` | `views/branch/addCustomer.vue` | editor / `persons.create` | `:119-126` |
| `/customer/list` | `list-customer` | `views/customer/single-customer/SingleCustomer.vue` | editor / `persons.list.client.single` | `:128-135` |
| `/report/order_item` | `report_order_item` | `views/report/order_item.vue` | editor / `report.order.item` | `:137-149` |
| `/report/product` | `report_product` | `views/report/product.vue` | editor / `report.product` | `:151-158` |
| `/finical/list` (**commented out**) | `finical_list` | `views/finical/list.vue` | editor / `document.list` | `:159-172` (inactive) |
**Branch full-page** (`src/router/branch.ts:175-184`): `/auth/login` → `page-login` → `views/branch/login.vue` (the `views/auth/Login.vue` import is commented out at `:178`), meta rule editor.
**Counts**: admin main-layout routes **73** (incl. 4 redirect-only), admin full-page **2**, shared error pages **4**, catch-all **1** → **80 admin-mode route records**. Branch mode: 15 active main-layout records + 1 full-page.
### 3.4 Guards
`router.afterEach` (`src/router/index.ts:63-68`) hides `#loading-bg`.
`getPermission()` (`:82-100`): if `usersStore.roles` already set, returns it; else `GET users/show` → `users.getRoles(data.permission)`, strips `permission` and `ut`, persists `api_token` under `tokenName` and the user blob under `userName`. Errors are swallowed with `console.log`.
`router.beforeEach` (`:111-129`):
1. `noneAuthUrl = ['/auth/login','/auth/forgot-password','/auth/register','/reg']` (`:71`) → allow.
2. No token in `localStorage[tokenName]` → redirect `{path:'/auth/login'}` (`:128`).
3. With a token: load roles; if `to.meta.role` is set and `!roles.includes(role)` → if the route name is `dashboard-ecommerce` or `dashboard-analytics`, redirect to `{name:'my-data'}`, else `{name:'page-not-authorized'}` (`:119-124`).
`meta.rule` (`editor`/`admin`) is **inert** — never read by any guard (legacy ACL plugin was not ported).
---
## 4. Menu / navigation
Defined in `src/layouts/navMenuItems.ts` (641 lines). Exports `adminMenu` (`:44`), `branchMenu` (`:551`), `getMenu()` selector (`:637-639`, `isBranch() ? branchMenu : adminMenu`). Node interface `NavMenuNode { url, name, slug, icon, i18n, tag, tagColor, permissionId, header, items, submenu }` (`:30-42`).
Rendered by `src/layouts/components/VerticalNavMenu.vue` (uses `a-menu-item-group` for `header` nodes, `:39`) and recursively by `src/layouts/components/NavMenuNode.vue` (`a-sub-menu` when `node.submenu`, `:8`). Icons resolved from legacy Feather names via `src/components/icons.ts` `resolveIcon()`.
**Visibility rule** (`VerticalNavMenu.vue:81-82`, `NavMenuNode.vue:36-37`): a node with **no** `permissionId` is always visible; otherwise it requires `users.hasPermission(permissionId)`.
### 4.1 Admin menu items
| Label (i18n) | url | icon | permissionId | line |
|---|---|---|---|---|
| پیشخوان | `/dashboard/ecommerce` | HomeIcon | `dashboard.view` | `:49-55` |
| **[header] کاربری** (`Apps`) | — | PackageIcon | — | `:58-60` |
| اشخاص | null | UserIcon | `persons` | `:63-67` |
| ├ اضافه کردن شخص | `/customer/add` | — | `persons.create` | `:73-77` |
| ├ لیست کارشناسان فروش | `/customer/reagent-list` | — | `persons.list.sellers` | `:80-84` |
| ├ لیست مشتریان عمده | `/customer/omde-list` | — | `persons.list.client.omde` | `:87-91` |
| ├ لیست مشتریان در انتظار | `/customer/omde-non-verified-list` | — | `persons.list.client.omde` | `:94-98` |
| ├ لیست مشتریان بایگانی | `/customer/omde-archived-list` | — | `persons.list.client.omde` | `:101-105` |
| ├ لیست مشتریان تک | `/customer/list` | — | `persons.list.client.single` | `:108-112` |
| ├ لیست بنکداران | `/customer/banker-list` | — | `persons.list.client.omde` (was commented out in legacy; filled in) | `:115-124` |
| └ لیست مغازه داران حضوری | `/customer/shopkeeper-list` | — | `persons.list.client.omde` (same fix) | `:130-136` |
| شعب و کارگاه | null | GridIcon | — | `:144-148` |
| ├ شعب | null | GridIcon | `branch` | `:150-154` |
| │ └ لیست شعب | `/branch/list` | — | `branch.list` | `:159-163` |
| ├ کارگاه | null | GridIcon | `workshop` | `:168-172` |
| │ └ لیست کارگاه ها | `/workshop/list` | — | `workshop.list` | `:177-181` |
| محصولات | null | ShoppingBagIcon | `product` | `:189-193` |
| ├ افزودن محصول | `/apps/product/add` | — | `product.create` | `:196-200` |
| ├ محصولات | `/apps/product/list` | — | `product.list` | `:203-207` |
| └ تغییر درصد محصولات | `/product/change/percent` | — | `product.percent` | `:210-214` |
| سفارش ها | null | BoxIcon | `order` | `:219-223` |
| ├ ثبت سفارش تک | `/order/create` | — | `order.create` | `:226-230` |
| ├ لیست سفارش های تک | `/order/list` | — | `order.list` | `:233-237` |
| ├ سفارش های ساختنی | `/order/make` | — | `order.makeOrder` | `:240-244` |
| ├ لیست سفارش های عمده | `/order/omde-list` | — | `order.omde` | `:247-251` |
| └ لیست تامین | `/order/tamin-list` | — | `order.tamin` | `:254-258` |
| مدیران | null | UserIcon | `user` | `:263-267` |
| ├ دسترسی ها | `/permission` | — | `user.permission` | `:270-274` |
| └ لیست مدیران | `/user/list` | — | `user.list` | `:279-283` |
| تخفیفات | null | GiftIcon | `offers` | `:288-292` |
| ├ تخفیف اتوماتیک | `/major/promotion/autopromotion` | — | `offers.autopromotion` | `:298-302` |
| ├ لیست تخفیفات | `/major/promotion/list` | — | `offers.list` | `:307-311` |
| └ استثناعات تخفیف | `/major/offersIgnore` | SlashIcon | `offersIgnore` | `:315-320` |
| گزارشات و اسناد مالی | null | FileTextIcon | — | `:329-333` |
| ├ اسناد مالی | null | DollarSignIcon | `document` | `:335-339` |
| │ └ لیست اسناد | `/finical/list` | — | `document.list` | `:342-346` |
| ├ فاکتور انتقال | null | BookIcon | `invoice` | `:354-358` |
| │ └ لیست فاکتورها | `/invoice/list` | — | `invoice.list` | `:363-367` |
| ├ گزارشات | null | FileTextIcon | `report` | `:372-376` |
| │ ├ گزارش تجمیعی سفارش و اسناد | `/report/order_item` | — | `report.order.item` | `:379-383` |
| │ └ گزارش محصولات | `/report/product` | — | `report.product` | `:386-390` |
| پیامک | null | MailIcon | `sms` | `:397-401` |
| └ ارسال پیامک | `/sms/create` | — | `sms.create` | `:404-408` |
| آمار | `/statics` | BarChartIcon | `statics` | `:413-417` |
| خبرنامه | `/newsletters` | FileTextIcon | `newsletters` (corrected from legacy's dead `newsletters.newsletters`) | `:420-429` |
| **[header] مدیریت** (`UI`) | — | LayersIcon | — | `:434-436` |
| پیکر بندی | null | SettingsIcon | `config` | `:439-443` |
| ├ مدیریت ثابت ها | `/config/manage/const` | — | `config.statics` | `:446-450` |
| ├ گزارش | `/config/manage/report` | — | `config.report` | `:453-457` |
| └ مدیریت کیف پول | `/config/manage/wallet` | — | `config.wallet` | `:460-464` |
| دسته بندی ها | null | ListIcon | `collection` | `:469-473` |
| └ لیست دسته بندی ها | `/category/list` | — | `collection.list` | `:478-482` |
| کیف پول | null | CreditCardIcon | `wallet` | `:487-491` |
| └ لیست کیف پول های شارژ شده | `/wallet` | — | `wallet.list` | `:494-498` |
| شهر ها | null | PercentIcon | `cities` | `:503-508` |
| └ لیست شهر ها | `/city/list` | — | `cities.list` | `:513-517` |
| دیدگاه ها | `/comments` | MessageSquareIcon | `comment` | `:522-527` |
| نوتیفیکیشن | null | BellIcon | `notify` | `:530-534` |
| └ ارسال نوتیفیکیشن | `/notify/create` | — | `notify.create` | `:537-541` |
### 4.2 Branch menu (`src/layouts/navMenuItems.ts:551-634`)
| Label | url | icon | permissionId | line |
|---|---|---|---|---|
| پیشخوان | `/dashboard` | HomeIcon | `dashboard` | `:552-559` |
| اشخاص | null | UserIcon | `persons` | `:560-566` |
| ├ اضافه کردن مشتری | `/customer/add` | UserPlusIcon | `persons.create` | `:567-573` |
| └ لیست مشتریان | `/customer/list` | UsersIcon | `persons.list.client.single` | `:575-581` |
| محصولات | `/products` | ShoppingBagIcon | `product.list` | `:584-591` |
| فاکتور خرید | `/buy/order/list` | ShoppingBagIcon | `product.list` | `:592-599` |
| سفارش ها | null | BoxIcon | `order` | `:609-614` |
| ├ ثبت سفارش | `/order/create` | InboxIcon | `order.create` | `:616-622` |
| └ لیست سفارش | `/order/list` | ListIcon | `order.list` | `:624-630` |
Note: `/wallet` menu item is gated on `wallet.list` while the **route** `/wallet` is gated on `wallet` (`src/router/admin.ts:458`) — a rid mismatch preserved from legacy.
---
## 5. Permissions
### 5.1 Mechanism
- **Store** `src/stores/users.ts`. State: `roles: string[] | undefined` (undefined = "not fetched"), `cityFilters`, `userInfo` (`:249-256`).
- **Parent expansion** `checkParent()` (`:220-229`) using `getParents()` (`:196-204`): granting `persons.list.client.omde` implicitly grants `persons.list.client`, `persons.list`, `persons`. It only walks **up**, never down — a user with `wallet` still fails a `wallet.list` check (documented legacy behaviour, `:216-218`).
- **Getter** `hasPermission(permissionId)` (`:263-269`): returns `roles.includes(id)` or `undefined` when roles unloaded (falsy in `v-if`). This is the port of the legacy global `Vue.prototype.$hasPermission`.
- **Action** `getRoles(roles)` commits `checkParent(roles)` (`:293-295`); `deleteRoles()` sets `undefined` (`:296-298`).
- **Sources of roles**: login response `data.roles.map(x => x.rid)` (`src/stores/auth.ts:152`), or `GET users/show` → `data.permission` (`src/router/index.ts:88`).
- **Route guard**: `to.meta.role` checked in `router.beforeEach` (`src/router/index.ts:119-124`).
- **Menu guard**: `node.permissionId` (`VerticalNavMenu.vue:81-82`, `NavMenuNode.vue:36-37`).
- **Template guard**: each view defines `const hasPermission = (rid: string) => Boolean(users.hasPermission(rid))` and uses `v-if`. E.g. `src/views/apps/product/AddProduct.vue:286`, `src/views/apps/product/ProductList.vue:113`.
- **Table-model guard**: some columns are added only if `users.roles.includes(...)` — `src/views/order/orderListModel.ts:24`, `src/views/order/OmdeListModel.ts:32`, `src/views/order/TaminListModel.ts:21`, `src/views/order/MakeOrderModel.ts:29`, `src/views/customer/Comments/CommentListModel.ts:21`.
- **Component guard**: `src/components/common/ListPageHeader.vue:146` renders the add button only when `!rid || hasPermission(rid)`.
- **No directives** (no `v-permission`) and **no composable** — only the store getter.
### 5.2 Permission vocabulary — `src/views/user/permissionConst.ts` (`treeData`, `:23`)
179 `key` entries, **174 unique** (5 duplicated: `persons.list.client.single.view` at `:131`/`:139`, `product.list.view` at `:191`/`:199`, `order.list.view` at `:252`/`:275`, `order.tamin.view` at `:386`/`:409`, `cities.list.view` at `:640`/`:648`).
| key | label | line |
|---|---|---|
| `dashboard.view` | پیشخوان | `:29` |
| `persons` | اشخاص | `:33` |
| `persons.create` | اضافه کردن شخص | `:37` |
| `persons.create.seller` | افزودن کارشناس فروش | `:41` |
| `persons.create.client` | افزودن مشتری عمده | `:45` |
| `persons.create.single` | افزودن مشتری تک | `:49` |
| `persons.list.sellers` | لیست کارشناسان فروش | `:55` |
| `persons.list.sellers.edit` | ویرایش اطلاعات کارشناس فروش | `:59` |
| `persons.list.sellers.view.orders` | مشاهده سفارشات کارشناس فروش | `:63` |
| `persons.list.sellers.view.documents` | مشاهده اسناد مالی کارشناس فروش | `:67` |
| `persons.list.sellers.unable.order` | توقف سفارش کارشناس فروش | `:71` |
| `persons.list.sellers.able.order` | فعال سازی سفارش کارشناس فروش | `:75` |
| `persons.list.sellers.able` | فعال سازی حساب کاربری کارشناس فروش | `:79` |
| `persons.list.sellers.unable` | غیر فعال سازی حساب کاربری کارشناس فروش | `:83` |
| `persons.list.client.omde` | لیست مشتریان عمده | `:89` |
| `persons.list.client.omde.view` | مشاهده اطلاعات مشتریان عمده | `:93` |
| `persons.list.client.omde.edit` | ویرایش اطلاعات مشتری عمده | `:97` |
| `persons.list.client.omde.view.orders` | مشاهده سفارشات مشتری عمده | `:101` |
| `persons.list.client.omde.view.documents` | مشاهده اسناد مالی مشتری عمده | `:105` |
| `persons.list.client.omde.unable.order` | توقف سفارش مشتری عمده | `:109` |
| `persons.list.client.omde.able.order` | فعال سازی سفارش مشتری عمده | `:113` |
| `persons.list.client.omde.able` | فعال سازی مشتری عمده | `:117` |
| `persons.list.client.omde.unable` | غیر فعال سازی مشتری عمده | `:121` |
| `persons.list.client.single` | لیست مشتریان تک | `:127` |
| `persons.list.client.single.view` | مشاهده اطلاعات مشتریان تک | `:131`, `:139` |
| `persons.list.client.single.edit` | ویرایش اطلاعات مشتری تک | `:135` |
| `branch` | شعب | `:147` |
| `branch.create` | افزودن شعب | `:152` |
| `branch.list` | لیست شعب | `:156` |
| `workshop` | کارگاه | `:162` |
| `workshop.create` | افزودن کارگاه | `:167` |
| `workshop.list` | لیست کارگاه ها | `:171` |
| `product` | محصولات | `:177` |
| `product.create` | افزودن محصول | `:182` |
| `product.list` | لیست محصولات | `:186` |
| `product.list.view` | مشاهده لیست محصولات / مشاهده اطلاعات جامع محصول | `:191`, `:199` |
| `product.list.edit` | ویرایش اطلاعات محصول | `:195` |
| `product.list.variants` | تنوع های محصول | `:203` |
| `product.list.variants.view` | مشاهده تنوع های محصول | `:208` |
| `product.list.variants.edit` | ویرایش تنوع محصول | `:212` |
| `product.list.variants.remove` | حذف تنوع محصول | `:216` |
| `product.list.variants.add` | افزودن تنوع محصول | `:220` |
| `product.list.remove` | حذف محصول | `:226` |
| `product.list.group.edit` | تغییر دسته ای | `:230` |
| `product.percent` | تغییر درصد محصولات | `:236` |
| `order` | سفارشات | `:242` |
| `order.list` | لیست سفارشات | `:247` |
| `order.list.view` | مشاهده لیست سفارشات / مشاهده سفارش | `:252`, `:275` |
| `order.list.sts` | وضعیت سفارش | `:256` |
| `order.list.sts.view` | مشاهده وضعیت سفارش | `:261` |
| `order.list.sts.remove` | حذف وضعیت | `:265` |
| `order.list.sts.submit` | ثبت وضعیت | `:269` |
| `order.list.view.edit` | ویرایش سفارش | `:279` |
| `order.list.pdf` | دریافت pdf | `:283` |
| `order.omde` | سفارشات عمده | `:289` |
| `order.omde.state` | وضعیت سفارش | `:294` |
| `order.omde.state.view` | مشاهده وضعیت سفارش | `:299` |
| `order.omde.state.submit` | ثبت وضعیت سفارش | `:303` |
| `order.omde.state.remove` | حذف وضعیت سفارش | `:307` |
| `order.omde.view` | مشاهده سفارش | `:313` |
| `order.omde.view.edit` | ویرایش سفارش | `:317` |
| `order.omde.view.remove` | حذف آیتم از سفارش | `:321` |
| `order.omde.remove` | حذف سفارش | `:325` |
| `order.omde.get` | دریافت pdf سفارش | `:329` |
| `order.makeOrder` | سفارشات ساختنی | `:335` |
| `order.makeOrder.state` | وضعیت سفارش | `:340` |
| `order.makeOrder.state.view` | مشاهده وضعیت سفارش | `:345` |
| `order.makeOrder.state.submit` | ثبت وضعیت سفارش | `:349` |
| `order.makeOrder.state.remove` | حذف وضعیت سفارش | `:353` |
| `order.makeOrder.view` | مشاهده سفارش | `:359` |
| `order.makeOrder.view.edit` | ویرایش سفارش | `:363` |
| `order.makeOrder.view.remove` | حذف آیتم از سفارش | `:367` |
| `order.makeOrder.remove` | حذف سفارش | `:371` |
| `order.makeOrder.get` | دریافت pdf سفارش | `:375` |
| `order.tamin` | لیست تامین | `:381` |
| `order.tamin.view` | مشاهده لیست تامین / مشاهده سفارش | `:386`, `:409` |
| `order.tamin.state` | وضعیت سفارش | `:390` |
| `order.tamin.state.view` | مشاهده وضعیت سفارش | `:395` |
| `order.tamin.state.submit` | ثبت وضعیت سفارش | `:399` |
| `order.tamin.state.remove` | حذف وضعیت سفارش | `:403` |
| `order.tamin.view.edit` | ویرایش سفارش | `:413` |
| `order.create` | ثبت سفارش تک | `:419` |
| `user` | مدیران | `:425` |
| `user.create` | افزودن مدیر | `:430` |
| `user.list` | لیست مدیران | `:434` |
| `user.list.view` | مشاهده لیست مدیران | `:439` |
| `user.list.edit` | ویرایش مدیر | `:443` |
| `user.list.remove` | حذف مدیر | `:447` |
| `user.list.create` | افزودن مدیر | `:451` |
| `user.permission` | دسترسی ها | `:457` |
| `user.permission.edit` | ویرایش دسترسی | `:462` |
| `user.permission.create` | افزودن دسترسی | `:466` |
| `collection` | دسته بندی ها | `:474` |
| `collection.create` | افزودن دسته بندی | `:479` |
| `collection.list` | لیست دسته بندی ها | `:483` |
| `collection.list.view` | مشاهده لیست دسته بندی ها | `:488` |
| `collection.list.edit` | ویرایش دسته | `:492` |
| `collection.list.remove` | حذف دسته | `:496` |
| `document` | اسناد مالی | `:504` |
| `document.list` | لیست اسناد مالی | `:509` |
| `document.list.view` | مشاهده لیست اسناد مالی | `:514` |
| `document.list.edit` | ویرایش سند مالی | `:518` |
| `document.list.remove` | حذف سند مالی | `:522` |
| `document.list.deny` | رد کردن سند مالی | `:526` |
| `document.list.confirm` | تأیید کردن سند مالی | `:530` |
| `document.list.get` | دریافت pdf سند مالی | `:534` |
| `document.create` | وارد کردن سند مالی مشتری | `:540` |
| `offersIgnore` | استثناعات تخفیف | `:546` |
| `offers` | تخفیفات | `:550` |
| `offers.list` | لیست تخفیفات | `:555` |
| `offers.list.view` | مشاهده لیست تخفیفات | `:560` |
| `offers.list.edit` | ویرایش تخفیف عمده | `:564` |
| `offers.list.tak.edit` | ویرایش تخفیف تک | `:568` |
| `offers.list.remove` | حذف تخفیف عمده | `:572` |
| `offers.list.tak.remove` | حذف تخفیف تک | `:576` |
| `offers.create` | تعریف تخفیف عمده | `:582` |
| `offers.create.tak` | تعریف تخفیف تک | `:586` |
| `offers.autopromotion` | تخفیف اتوماتیک | `:590` |
| `invoice` | فاکتور انتقال | `:596` |
| `invoice.create` | افزودن فاکتور | `:601` |
| `invoice.list` | لیست فاکتورها | `:605` |
| `report` | گزارشات | `:611` |
| `report.order.item` | گزارش تجمیعی سفارشات و اسناد مالی | `:616` |
| `report.product` | گزارش محصولات | `:620` |
| `cities` | شهرها | `:626` |
| `cities.create` | افزودن شهر | `:631` |
| `cities.list` | لیست شهرها | `:635` |
| `cities.list.view` | مشاهده لیست شهرها / مشاهده شهر | `:640`, `:648` |
| `cities.list.remove` | حذف شهر | `:644` |
| `cities.list.view.edit` | ویرایش شهر | `:652` |
| `comment` | دیدگاه ها | `:660` |
| `comment.view.all` | مشاهده لیست دیدگاه ها | `:664` |
| `comment.state` | تغییر وضعیت دیدگاه | `:668` |
| `comment.view` | مشاهده دیدگاه | `:672` |
| `comment.view.edit` | ویرایش دیدگاه | `:676` |
| `comment.remove` | حذف دیدگاه | `:680` |
| `sms` | پیامک | `:686` |
| `sms.create` | ارسال پیامک | `:691` |
| `sms.create.single` | ارسال برای یک شماره تلفن | `:696` |
| `sms.create.all` | ارسال برای همه کاربران | `:700` |
| `sms.create.reagent` | ارسال برای کارشناسان فروش | `:704` |
| `sms.create.omde` | ارسال برای مشتریان عمده | `:708` |
| `sms.create.multiple` | ارسال برای چند شماره تلفن | `:712` |
| `sms.list` | لیست پیامک های ارسال شده | `:718` |
| `wallet` | کیف پول | `:725` |
| `wallet.list` | لیست کیف پول های شارژ شده | `:730` |
| `wallet.charge` | شارژ کیف پول | `:734` |
| `wallet.percent` | تغییر درصد شارژ کیف پول | `:738` |
| `notify` | نوتیفیکیشن | `:744` |
| `notify.create` | ارسال نوتیفیکیشن | `:749` |
| `notify.create.multiple.reagent` | ارسال برای کارشناسان فروش | `:754` |
| `notify.create.single.customer` | ارسال برای یک مشتری | `:758` |
| `notify.create.multiple.omde` | ارسال برای مشتریان عمده | `:762` |
| `notify.create.multiple.customer` | ارسال برای همه مشتریان | `:766` |
| `notify.create.single.admin` | ارسال برای یک مدیر | `:770` |
| `notify.create.multiple.admin` | ارسال برای همه مدیران | `:774` |
| `notify.me` | لیست نوتیفیکیشن های دریافتی | `:780` |
| `config` | پیکربندی | `:786` |
| `config.statics` | مدیریت ثابت ها | `:790` |
| `config.statics.brand` | برند ها | `:795` |
| `config.statics.brand.create` | افزودن برند | `:800` |
| `config.statics.brand.edit` | ویرایش برند | `:804` |
| `config.statics.brand.remove` | حذف برند | `:808` |
| `config.statics.tags` | برچسب ها | `:814` |
| `config.statics.tags.create` | افزودن برچسب | `:819` |
| `config.statics.tags.edit` | ویرایش برچسب | `:823` |
| `config.statics.tags.remove` | حذف برچسب | `:827` |
| `config.wallet` | مدیریت کیف پول | `:835` |
| `config.report` | گزارش | `:839` |
| `statics` | آمار | `:845` |
| `newsletters.newsletters` | خبرنامه | `:849` |
| `custom` | سفارشی | `:853` |
| `custom.config.statics.slider` | اسلایدر موبایل | `:857` |
| `custom.config.statics.tags.banner.ids` | برچسب بنر اول سایت | `:861` |
**Known gaps** (`src/views/user/permissionConst.ts:6-15`): 4 rids are enforced but **absent** from the tree and cannot be granted from the UI — `buy.order.list`, `newsletters`, `persons.create.customer.tak`, `persons.list.sellers.view`. ~36 tree keys are never checked anywhere.
### 5.3 rids checked via `hasPermission()` in templates (73 distinct)
`cities.list.remove`, `cities.list.view.edit`, `collection.list.edit`, `collection.list.remove`, `comment.remove`, `comment.state`, `comment.view`, `comment.view.edit`, `config.statics`, `config.statics.brand.create`, `config.statics.brand.edit`, `config.statics.brand.remove`, `config.statics.tags.create`, `config.statics.tags.remove`, `config.wallet`, `custom.config.statics.slider`, `document.list.confirm`, `document.list.deny`, `document.list.edit`, `document.list.remove`, `offers.list.edit`, `offers.list.remove`, `offers.list.tak.edit`, `offers.list.tak.remove`, `order.create`, `order.list.pdf`, `order.list.sts`, `order.list.sts.remove`, `order.list.sts.submit`, `order.list.view`, `order.list.view.edit`, `order.makeOrder.state`, `order.makeOrder.view`, `order.makeOrder.view.edit`, `order.omde.remove`, `order.omde.state`, `order.omde.state.remove`, `order.omde.state.submit`, `order.omde.view`, `order.tamin.state`, `order.tamin.state.remove`, `order.tamin.state.submit`, `order.tamin.view`, `persons.create`, `persons.create.client`, `persons.create.seller`, `persons.list.client.omde.able`, `persons.list.client.omde.able.order`, `persons.list.client.omde.edit`, `persons.list.client.omde.view`, `persons.list.client.omde.view.documents`, `persons.list.client.omde.view.orders`, `persons.list.sellers.able`, `persons.list.sellers.edit`, `persons.list.sellers.unable`, `persons.list.sellers.view`, `product.list.edit`, `product.list.remove`, `product.list.variants`, `product.list.variants.add`, `product.list.variants.edit`, `product.list.variants.remove`, `product.list.view`, `sms.create.all`, `sms.create.multiple`, `sms.create.omde`, `sms.create.reagent`, `sms.create.single`, `sms.list`, `user.list.edit`, `user.list.remove`, `user.permission`, `user.permission.create`, `user.permission.edit`, `wallet.charge`, `wallet.percent`.
---
## 6. Views — full recursive inventory of `src/views`
### 6.1 Every file (110 files; 91 `.vue`, 19 `.ts`)
| file | LOC | purpose |
|---|---|---|
| `src/views/Dashboard.vue` | 327 | **PROTECTED.** Merged wholesale+retail dashboard; see §10a. |
| `src/views/apps/category/Add.vue` | 261 | Category create/edit form (shared by Edit.vue). |
| `src/views/apps/category/CategoryTbl.ts` | 21 | XTbl model for the category grid (`admin/categories/list`). |
| `src/views/apps/category/Edit.vue` | 13 | Thin wrapper over `Add.vue` for edit mode. |
| `src/views/apps/category/List.vue` | 87 | Category grid + row actions + delete. |
| `src/views/apps/customer/AddCustomer.vue` | 47 | Tabbed shell picking retail/wholesale/reagent customer forms. |
| `src/views/apps/customer/EditBulkCustomer.vue` | 13 | Wrapper → `bulkCustomer.vue` in edit mode. |
| `src/views/apps/customer/EditReagent.vue` | 12 | Wrapper → `ReagentCustomer.vue`. |
| `src/views/apps/customer/EditSingleCustomer.vue` | 13 | Wrapper → `singleCustomer.vue` in edit mode. |
| `src/views/apps/customer/ReagentCustomer.vue` | 205 | Sales-agent (کارشناس فروش) create/edit form. |
| `src/views/apps/customer/bulkCustomer.vue` | 472 | Wholesale customer create/edit form. |
| `src/views/apps/customer/singleCustomer.vue` | 136 | Retail customer create/edit form. |
| `src/views/apps/product/AddProduct.vue` | 579 | **PROTECTED (variants).** Product create/edit; see §10b. |
| `src/views/apps/product/ChangePercent.vue` | 136 | Bulk percentage-wage change across brand/category. |
| `src/views/apps/product/EditProduct.vue` | 17 | Wrapper → `AddProduct.vue` with route `:id`. |
| `src/views/apps/product/ProductList.vue` | 168 | Product grid, row actions, detail modal, delete. |
| `src/views/apps/product/productListModel.ts` | 31 | XTbl model for `admin/products/list`. |
| `src/views/auth/Login.vue` | 162 | Admin login (mobile + password). |
| `src/views/branch/AddBranches.vue` | 184 | Branch create/edit form. |
| `src/views/branch/EditBranches.vue` | 10 | Wrapper → `AddBranches.vue`. |
| `src/views/branch/FacilityList.vue` | 112 | Shared branch/workshop list implementation with active toggle. |
| `src/views/branch/ListBranch.ts` | 20 | XTbl model `admin/branches/list`. |
| `src/views/branch/ListBranches.vue` | 25 | Wrapper → `FacilityList.vue` for branches. |
| `src/views/branch/addCustomer.vue` | 14 | Branch-mode customer add wrapper. |
| `src/views/branch/buyList.vue` | 80 | Branch "فاکتور خرید" allocations list. |
| `src/views/branch/dashboard.vue` | 93 | Branch-mode dashboard (retail analytics + top 5 products). |
| `src/views/branch/login.vue` | 105 | Branch-mode login. |
| `src/views/branch/order/create.vue` | 253 | Branch order builder. |
| `src/views/branch/orderListModel.ts` | 20 | XTbl model `admin/invoices/list` (branch buy list). |
| `src/views/city/CityListModel.ts` | 18 | XTbl model `admin/misc/cities/list`. |
| `src/views/city/cityList.vue` | 127 | City list with inline percent editing + delete. |
| `src/views/city/createCity.vue` | 102 | City create form. |
| `src/views/customer/Comments/CommentListModel.ts` | 30 | XTbl model `admin/comments/list`. |
| `src/views/customer/Comments/Comments.vue` | 274 | Product-comment moderation (approve/reject/reply/delete). |
| `src/views/customer/CustomerDetail.vue` | 211 | Shared customer detail (omde + single) with orders/docs/log tabs. |
| `src/views/customer/omde/BankerList.vue` | 13 | Wrapper → `OmdeCustomerList` source `banker`. |
| `src/views/customer/omde/OmdeArchivedList.vue` | 13 | Wrapper → source `archived`. |
| `src/views/customer/omde/OmdeCustomerList.vue` | 448 | Shared implementation of the 5 wholesale-customer lists. |
| `src/views/customer/omde/OmdeList.vue` | 18 | Wrapper → source `omde`. |
| `src/views/customer/omde/OmdeNonVerifiedList.vue` | 13 | Wrapper → source `nonVerified`. |
| `src/views/customer/omde/ShopkeeperList.vue` | 13 | Wrapper → source `shopkeeper`. |
| `src/views/customer/omde/ViewOmdeCustomer.vue` | 18 | Wrapper → `CustomerDetail.vue` (omde mode). |
| `src/views/customer/omde/omdeListModel.ts` | 71 | Factory building all 5 omde list models. |
| `src/views/customer/reagentList/AgentDetail.vue` | 127 | Sales-agent detail view. |
| `src/views/customer/reagentList/ReagentList.vue` | 128 | Sales-agent list + active toggle. |
| `src/views/customer/reagentList/ReagentListModel.ts` | 48 | XTbl model `admin/customers-omde/reagent/list`. |
| `src/views/customer/single-customer/SingleCustomer.vue` | 131 | Retail customer list + wallet charge modal. |
| `src/views/customer/single-customer/SingleCustomerListModel.ts` | 17 | XTbl model `admin/customers-omde/single/list`. |
| `src/views/customer/single-customer/ViewSingleCustomer.vue` | 19 | Wrapper → `CustomerDetail.vue` (single mode). |
| `src/views/finical/addFinDoc.vue` | 358 | Financial-document create/edit (5 document types). |
| `src/views/finical/list.vue` | 135 | Financial-document list + confirm/deny + delete. |
| `src/views/finical/list_model.ts` | 39 | XTbl model `admin/documents/list`. |
| `src/views/invoice/AddInvoice.ts` | 20 | Product-picker table model for the invoice builder. |
| `src/views/invoice/AddInvoice.vue` | 288 | Branch-allocation (فاکتور) builder. |
| `src/views/invoice/EditInvoice.vue` | 15 | Wrapper → `AddInvoice.vue`. |
| `src/views/invoice/ListInvoice.ts` | 19 | XTbl model `admin/invoices/list`. |
| `src/views/invoice/ListInvoice.vue` | 130 | Invoice list + detail modal + delete. |
| `src/views/manage/Const.vue` | 384 | Settings: brands / tags / mobile slider / collections tabs. |
| `src/views/manage/report.vue` | 43 | Dashboard-link shortcut board. |
| `src/views/manage/wallet.vue` | 157 | Wallet config (expiry/percent) + birthday config. |
| `src/views/newsletters/newsletter.ts` | 17 | XTbl model `admin/misc/newsletter/list`. |
| `src/views/newsletters/newsletters.vue` | 27 | Newsletter subscriber list. |
| `src/views/notify/create.vue` | 60 | Send-notification form. |
| `src/views/notify/notifyTbl.ts` | 16 | XTbl model `admin/misc/notify/list` (`noFilters()`). |
| `src/views/notify/user.vue` | 94 | Received notifications + mark-read. |
| `src/views/offersIgnore/offersIgnore-add-edit.vue` | 225 | Promotion exclusion (استثناعات) manager. |
| `src/views/order/List.vue` | 263 | Retail order list + status edit + PDF (stubbed). |
| `src/views/order/MakeOrder.vue` | 224 | Made-to-order (ساختنی) pre-order list + workshop assign. |
| `src/views/order/MakeOrderModel.ts` | 38 | XTbl model `admin/pre-orders/list`. |
| `src/views/order/OmdeList.vue` | 238 | Wholesale order list + status edit + delete. |
| `src/views/order/OmdeListModel.ts` | 57 | XTbl model `admin/orders/wholesale/list`. |
| `src/views/order/TaminList.vue` | 209 | Supply (تامین) order list + status edit. |
| `src/views/order/TaminListModel.ts` | 38 | XTbl model `admin/orders/tamin/list`. |
| `src/views/order/create.vue` | 425 | Admin order builder (cart + custom variant weight). |
| `src/views/order/orderListModel.ts` | 34 | XTbl model `admin/orders/list`. |
| `src/views/pages/Error404.vue` | 20 | 404 page. |
| `src/views/pages/Error500.vue` | 20 | 500 page. |
| `src/views/pages/Maintenance.vue` | 20 | Maintenance page. |
| `src/views/pages/NotAuthorized.vue` | 20 | 403 page. |
| `src/views/promotion/PromotionForm.vue` | 408 | Shared omde/tak promotion create-edit form. |
| `src/views/promotion/autopromotion.vue` | 79 | Automatic-discount percent config. |
| `src/views/promotion/promotionListModel.ts` | 18 | XTbl model `admin/promotions/list`. |
| `src/views/promotion/promotion_add_edit.vue` | 26 | Wrapper → `PromotionForm` (omde mode). |
| `src/views/promotion/promotion_list.vue` | 145 | Promotion list + edit routing + delete. |
| `src/views/promotion/promotion_tak_add_edit.vue` | 26 | Wrapper → `PromotionForm` (tak mode). |
| `src/views/reg.vue` | 121 | **Public** self-registration form (no auth guard). |
| `src/views/report/order_item.vue` | 100 | Aggregated order+document report with date filters. |
| `src/views/report/product.vue` | 84 | Per-product sales report. |
| `src/views/sms/create.vue` | 115 | SMS composer + sent log. |
| `src/views/sms/smsTbl.ts` | 17 | XTbl model `admin/misc/sms/list`. |
| `src/views/statics/index.vue` | 91 | Customer activity log viewer. |
| `src/views/user/ListUser.vue` | 98 | Admin user list + delete. |
| `src/views/user/Permission.vue` | 215 | Permission tree editor per user type. |
| `src/views/user/UserCreate.vue` | 279 | Admin user create/edit. |
| `src/views/user/UserEdit.vue` | 13 | Wrapper → `UserCreate.vue`. |
| `src/views/user/myData.vue` | 45 | Current-user profile ("my data"). |
| `src/views/user/permissionConst.ts` | 866 | The 179-entry permission tree (`treeData`). |
| `src/views/user/userTbl.ts` | 18 | XTbl model `admin/users/list`. |
| `src/views/wallet/wallet.vue` | 44 | Charged-wallet list. |
| `src/views/wallet/walletTbl.ts` | 16 | XTbl model `admin/documents/wallet/list` (`noFilters()`). |
| `src/views/workshop/AddWorkshop.vue` | 189 | Workshop create/edit. |
| `src/views/workshop/EditWorkshop.vue` | 10 | Wrapper → `AddWorkshop.vue`. |
| `src/views/workshop/ListWorkshop.ts` | 14 | XTbl model `admin/workshops/list`. |
| `src/views/workshop/ListWorkshop.vue` | 25 | Wrapper → `FacilityList.vue` for workshops. |
### 6.2 Significant views — detail
#### `src/views/auth/Login.vue`
Fields: `موبایل` (`a-input`, `:23-28`), `رمز عبور` (`a-input-password`, `:36-41`, submits on Enter).
Validation (ported VeeValidate `required|min:8` / `required|min:5`, on blur — `:79-92`): empty → `وارد کردن موبایل الزامی است` / `وارد کردن رمز عبور الزامی است`; too short → `مقدار ورودی صحیح نیست`. Submit disabled until `validateForm` (`:95-97`).
Action: `auth.login({phone, password})` → `POST admin-login`; on success `router.push('/dashboard/ecommerce')` (`:105-108`); on failure `message.error('خطا در ورود')` (`:111`). Legacy's `token_mobile` (Firebase push) intentionally omitted (`:103-104`).
#### `src/views/apps/product/ProductList.vue`
Table model `buildProductListModel()` → `POST admin/products/list`. Columns (`productListModel.ts:10-26`): `id` "کد" (filterNumber), `ProductImage` "تصویر محصول" (slot `image`, noSort), `title` "نام محصول" (filterText), `brand.title` "برند" (filterText), `category.collection.title` "دسته بندی" (filterText), `product_code` "کد محصول" (filterText), `show_type` "نوع نمایش" (filterSelect `{1:'سایت',2:'گوشی',3:'سایت و گوشی'}`, slot `show_type`), `action` "عملیات" (slot `op`, noSort).
Selection enabled (`has-selection`, `:15`) with a count banner `تعداد N محصول انتخاب شده است.` (`:9`).
Row actions (`:31-56`, all hidden in branch mode via `v-if="!is_branch"`): ویرایش (`product.list.edit` → `/apps/product/edit/{id}`), اطلاعات جامع (`product.list.view` → `GET admin/products/{id}` → modal), حذف (`product.list.remove`, `a-popconfirm` بله/خیر, title `آیا مطمئن به حذف محصول مورد نظر هستید ؟`).
Delete: `POST delete/product/{id}` via `useDeleteHandler` (`:138-147`), success `محصول حذف شد` (`message.info`), fallback error `خطا در حذف محصول`.
Detail modal (`:60-90`, width 70%): descriptions نام محصول / کد محصول / برند / دسته بندی / نوع نمایش / تعداد تنوع, plus a variant sub-table (عنوان، وزن، اجرت درصدی، تعداد — `:123-128`).
**No export.**
#### `src/views/apps/product/ChangePercent.vue`
Bulk percentage-wage change. Loads `GET brand` + `POST category` (`:121`). `POST change/percent/count` previews affected count (`:87`); `POST change/all` applies (`:104`). Validation: empty percent → `message.error('لطفا اجرت درصدی را وارد کنید')` (`:99`).
#### `src/views/apps/category/Add.vue` (+ `Edit.vue`)
Loads the category tree `GET admin/categories/tree` (`:235`) and, in edit mode, `GET admin/categories/{id}` (`:243`).
Validation (`:205-206`): `!form.type` → `لطفا نوع نمایش دسته بندی را انتخاب کنید`; `!form.title` → `لطفا مقدار را وارد کنید`.
Submit: `PUT category/{id}` (edit, `:215`) / `POST category/` (create, `:217`); success `اطلاعات با موفقیت به ثبت رسید.` (`:219`), error `message.error(server ?? 'خطا در ثبت اطلاعات.')` (`:227`).
#### `src/views/apps/category/List.vue`
Table `admin/categories/list` (`CategoryTbl.ts:9`). Columns: `id` شماره (filterNumber), `image` تصویر (slot, noSort), `type_str` نوع نمایش, `title` عنوان دسته (**filterNumber** — legacy quirk preserved, `:14`), `parent.title` پدر دسته (filterNumber), `opt` عملیات (slot). Row actions gated on `collection.list.edit` / `collection.list.remove`. Delete via `useDeleteHandler`, success `دسته حذف شد` (`:69-71`).
#### `src/views/apps/customer/bulkCustomer.vue` (wholesale customer form)
Loads `GET query/reagent` (agent list, `:422`) and `POST cities/list` (`:428`); edit mode `GET customer/{id}` (`:435`).
Validation (`:377-379`): `fname`, `lname`, `mobile` each → `لطفا مقدار را وارد کنید`.
Submit `PUT customer/{id}` / `POST customer/create` (`:400-402`); success `اطلاعات با موفقیت به ثبت رسید.` (`:404`); error prefers a mobile-specific server message (`:414`).
#### `src/views/apps/customer/singleCustomer.vue` (retail customer form)
`POST cities/list` (`:112`), edit `GET customer/{id}` (`:120`). Submit `PUT single/{id}` / `POST single` (`:97-99`) — the `single` endpoint sets `kind=RETAIL` server-side (see `endpoints.ts:68-75`).
#### `src/views/apps/customer/ReagentCustomer.vue` (sales agent form)
Edit `GET admin/users/{id}` (`:171`); submit `PUT users/{id}` / `POST users` (`:154-156`).
#### `src/views/order/List.vue` (retail orders)
Table `admin/orders/list` (`orderListModel.ts:12`). Columns: `id` شماره (filterNumber); `customer.fname` نام مشتری (filterKey `customer.fullName`, filterText); `customer.lname` نام خانوادگی (same filterKey); `total_price` جمع کل سفارش (slot, filterText); `items` تعداد آیتم ها (slot `items_len`, noSort); `caj` تاریخ ثبت (filterKey `createdAt`, filterDate, noSort); `uaj` تاریخ ویرایش (hidden, filterKey `updatedAt`, filterDate); `user` ثبت کننده (slot, noSort); **conditional on `order.tamin.state`** (`:24`) → `sts_str` وضعیت (slot `sts`) + `branch.name` نام شعبه (filterText); `opt` عملیات (slot).
Detail: `GET admin/orders/{id}` (`:197`). Status edits `POST single/order-sts/edit` (`:203`, `:215`) — success `ویرایش موفق آمیز`, error `خطا در ویرایش`. PDF action is stubbed: `message.warning('خروجی PDF هنوز در بک‌اند جدید پیاده‌سازی نشده است')` (`:245`), gated on `order.list.pdf`.
#### `src/views/order/OmdeList.vue` (wholesale orders)
Table `admin/orders/wholesale/list`. Columns (`OmdeListModel.ts:19-52`): `id` شماره; `customer.name` مشتری (filterKey `customer.fullName`); `customer.mobile` موبایل (hidden); `input` جمع کل سفارش (filterKey `totalGoldWeightGrams`); `discount` جمع کل تخفیف (درصد); `customer.city_id` شهر (hidden, slot `city` — the `filterSelect(cityFilters)` call stays commented out, legacy quirk `:26`); `items` تعداد آیتم ها; `caj`/`uaj` dates; **conditional on `order.omde.state`** → `sts` وضعیت (slot `sts_str`, filterKey `status`, filterSelect `{0:'معلق',1:'ثبت سفارش',2:'تایید مدیر سیستم',3:'در حال تامین',4:'در حال آماده سازی ',5:'تحویل مسئول فروش',6:'تحویل مشتری '}`); `confirmed` وضعیت تایید مشتری (filterSelect `{1:'تایید شده',2:'در انتظار تایید'}`); `opt` عملیات.
Detail `GET admin/orders/{id}` (`:171`); status `POST order-sts/edit` (`:187`, `:198`); delete → `سفارش با موفقیت حذف گردید` (`:217`), gated `order.omde.remove`.
#### `src/views/order/TaminList.vue`
Table `admin/orders/tamin/list`, title **`لیست سفارش های عمده`** (same as OmdeList — legacy quirk, `TaminListModel.ts:3-4,11`). Columns add `wage` اجرت (filterKey `wageWeightGrams`). The status column is gated on `order.tamin.state` and calls **`filterSelect()` with no argument** → an empty dropdown (legacy quirk, `:22-24`).
#### `src/views/order/MakeOrder.vue`
Table `admin/pre-orders/list` (`MakeOrderModel.ts:13`). Columns: `customer.id` شماره (filterKey `id`), `customer.fname`/`customer.lname` (filterKey `customer.fullName`), `color` رنگ محصول (filterKey `colorName`), `size` سایز محصول (`sizeSpec`), `color_mina` رنگ مینا (`minaColor`), `color_stone` رنگ سنگ (`stoneColor`), `sum` تعداد, `time_creat_fa` تاریخ ثبت (`createdAt`, date), `time_update_fa` (hidden), `customer.name` ثبت کننده (slot), **conditional on `order.tamin.state` (NOT `order.makeOrder.state`** — legacy quirk `:28-29`) → `sts_str_fa` وضعیت; `opt` عملیات.
Workshop list `POST workshop/table {page:1,limit:100}` (`:168`); detail `GET admin/pre-orders/{id}` (`:177`); update `PUT custom-order/{id}` (`:193`).
#### `src/views/order/create.vue` (admin order builder, 425 LOC)
Loads `GET order/type` and `POST single-customer {page:1,limit:500}` (`:394-395`).
Form fields (`:126-161`): نوع فروش (select, validated), نام دریافت کننده, نام خانوادگی دریافت کننده, استان, شهر, کد پستی, تلفن همراه, تلفن ثابت, توضیحات, آدرس.
Validation (`:350-352`): `customer_id`, `sts`, `type` each empty → `مقدار را وارد نمایید`.
Cart table has a `حذف` column (`:251`, width 60, centered). Adding an item → `message.info('اضافه شد')` (`:308`). Custom weight → `POST add/variant` (`:327`), error `خطا در ثبت وزن` (`:336`). Submit `POST order` (`:373`); success `عملیات با موفقیت انجام شد` (`:380`). Cart state from `src/stores/cart.ts`.
#### `src/views/finical/addFinDoc.vue` (358 LOC)
Type-dependent form (`:30` نوع سند select). Per-type fields (`:60-118`): **assay** — وزن (گرم), عیار, نام آزمایشگاه, انگ, وزن نهایی اعمال شده; **transfer** — وزن (گرم), حواله به; **cheque** — مبلغ چک, وزن (گرم), نام صاحب حساب, نام بانک; **cash** — میزان پول, مظنه; always توضیحات (`:116`).
Customer lookup `GET query/customer` (`:325`); edit `GET cash/{id}` (`:333`).
Validation (`:296`): `!form.customer_id` → `مقدار را وارد نمایید`.
Submit `PUT cash/{id}` / `POST cash` (`:305-307`); success `اطلاعات با موفقیت به ثبت رسید.`
#### `src/views/finical/list.vue`
Table `admin/documents/list` (`list_model.ts:9`). Columns: `id` شماره سفارش (filterNumber); `type_str` نوع سند (filterText); `price` پول (**filterDate** — legacy quirk, `:13`); `value` وزن نهایی (گرم) (**filterDate**); `to` به (hidden); `customer.name` نام و نام خانوادگی مشتری; `note` توضیحات; `ayar` عیار (filterNumber); `lab` آزمایشگاه; `recive_date` تاریخ دریافت (filterDate); `account_name` نام حساب (hidden); `bank_name` بانک (hidden); `caj` تاریخ ثبت; `uaj` بروزرسانی (hidden); `confirmed` وضعیت تایید سند (slot, filterSelect `{0:'تایید نشده',1:'تایید شده'}`); `price_per_gram` مظنه (hidden); `original_value` وزن طلای داده شده (hidden); `ang` انگ (hidden); `opt` عملیات.
Confirm `POST cash/confirem/{id}` (`:111`) → `تغییرات با موفقیت ثبت شد`. Actions gated on `document.list.confirm`, `document.list.deny`, `document.list.edit`, `document.list.remove`. Delete → `سند حذف شد` (`:124-126`).
#### `src/views/customer/omde/OmdeCustomerList.vue` (448 LOC, backs 5 routes)
Table model via `buildOmdeListModel(source)`; `source` ∈ `omde|banker|shopkeeper|nonVerified|archived` → urls in `omdeListModel.ts:12-18`.
Columns (`omdeListModel.ts:34-65`): `id` کد (filterNumber); `fname`/`lname` (filterText, hidden); `name` نام و نام خانوادگی (filterText); `mobile` موبایل (hidden, noSort); `city_id` شهر (hidden, slot `city`, filterSelect from `users.cityFilters`); `reagent.name` کارشناس فروش; `gram_level` سطح اعتبار; `percent` درصد; `remain` بدهی گرمی; `rial_remain` بدهی ریالی; `caj` تاریخ ثبت (filterDate); `uaj` بروزرسانی (hidden); `sts_str` وضعیت ثبت سفارش (slot `sts`); `active` وضعیت حساب کاربری (slot, filterSelect `{'1':'فعال','-1':'غیر فعال'}`); `phone`, `address`, `store_name`, `zone`, `pasazh_name` (all hidden); `opt` عملیات.
Actions: verify `POST major/verify` (`:352`); toggle account `POST customer/toggle-active` (`:391`); toggle order permission `POST customer/toggle-sts` (`:403`); archive `GET major/archive/{id}` (`:413`). All success toasts `تغییرات ثبت شد ` (with trailing space). Errors: `خطا در تغییر وضعیت`, `خطا در بایگانی`.
`OmdeNonVerifiedList.vue:5` notes a preserved legacy quirk: the step-one confirm **rejects** instead of accepting.
#### `src/views/customer/single-customer/SingleCustomer.vue`
Table `admin/customers-omde/single/list`. Columns (`SingleCustomerListModel.ts:6-12`): `id` کد (filterNumber); `fname` نام (filterKey `fullName`); `lname` نام خانوادگی (filterKey `fullName`, noSort); `phone` تلفن تماس (filterKey `mobile`, noSort); `email` ایمیل (noSort); `branch.name` نام شعبه (noSort); `op` عملیات.
Wallet-charge modal → `POST wallet/charge` (`:99`), gated `wallet.charge`; empty value → `لطفا مقدار را وارد کنید` (`:94`); success `عملیات با موفقیت انجام شد` (`:103`).
#### `src/views/customer/reagentList/ReagentList.vue`
Table `admin/customers-omde/reagent/list`. Columns per `ReagentListModel.ts:16-43`, incl. the legacy quirk that `name` is added twice (visible `نام و نام خانوادگی` `:18`, hidden `نام` `:20`), plus `grams_sell` میزان فروش. Toggle active → `PUT users/{id} {is_active}` (`:109`).
#### `src/views/customer/CustomerDetail.vue`
Parallel loads (`:174-177`): `GET customer/{id}`, `GET major/dashboard/{id}` (catch→undefined), `GET admin/customers-omde/{id}/orders` (catch→[]), `GET admin/customers-omde/{id}/documents` (catch→[]), plus lazily `GET log/ago/{id}` (`:191`). Orders/documents tabs gated on `persons.list.client.omde.view.orders` / `.view.documents`.
#### `src/views/customer/Comments/Comments.vue`
Table `admin/comments/list`; `changeState` column added only with `comment.state` (`CommentListModel.ts:21-23`).
Status change `POST comment/sts {id, sts}` (`:198`) → `تغییرات ثبت شد `; guards `حالت فعلی تایید می باشد` / `حالت فعلی عدم تایید می باشد` when already in that state (`:50`, `:61`). Delete `DELETE comment/{id}` (`:215`) → `دیدگاه حذف شد` / `خطا در حذف`. Reply `POST comment` (`:225`).
#### `src/views/invoice/AddInvoice.vue` / `ListInvoice.vue`
Builder: branch list `GET branch/list` (`:253`); edit `GET branch/product/{id}` (`:261`); submit `PUT branch/product/{id}` / `POST branch/product` with `{...form, products}` (`:231-233`). Validation `!form.branch_id` → `مقدار را وارد نمایید` (`:224`). Cart from `src/stores/invoiceCart.ts`. Item table has a `حذف` column (`:202`); add → `اضافه شد` (`:214`); success `عملیات با موفقیت انجام شد` (`:235`).
List columns (`ListInvoice.ts:7-14`): `id` ردیف; `name` نام مشتری (filterKey `branch.name`); `sum_weight` جمع کل سفارش; `count_item` تعداد آیتم ها; `caj` تاریخ ثبت (filterKey `createdAt`, filterDate); `allocator_user.name` ثبت کننده (slot `user`); `note` توضیحات; `op` عملیات (slot `actions`). Detail `GET branch/product/{id}` (`:113`); delete → `فاکتور حذف شد` (`:118-120`).
#### `src/views/user/Permission.vue`
Loads user types `GET const/__list__ut` (`:118`), then the type's rids `GET roles/{id}` (`:136`), rendering `treeData` as a checkable `a-tree`.
Validation (`:147`, `:151`): `!ut` → `لطفاسطح دسترسی را انتخاب کنید`; `!user` → `لطفا نام سطح دسترسی را وارد کنید`.
Create user type `POST const` (`:160`); save rids `POST roles/create {ut, roles}` (`:167`) → `عملیات با موفقیت انجام شد`. Gated on `user.permission`, `user.permission.create`, `user.permission.edit`.
#### `src/views/user/UserCreate.vue` / `ListUser.vue`
Create: parallel `GET const/__list__ut` + `GET admin/users/role-options` (`:241-242`); edit `GET admin/users/{id}` (`:252`). Validation: password mismatch → `رمز عبور و تأیید آن یکسان نیستند` (`:200`). Submit `PUT users/{id}` / `POST users` (`:220-222`).
List columns (`userTbl.ts:6-13`): `id` کد مدیر; `fname` نام (filterKey `fullName`); `lname` نام خانوادگی (filterKey `fullName`); `mobile` موبایل (filterText); `ut` نوع کاربر (filterNumber, slot `ut`); `op` عملیات. Delete → `کاربر حذف شد` (`:82-84`), gated `user.list.remove` / `user.list.edit`.
#### `src/views/manage/Const.vue` (384 LOC) — 4 tabs
`برند ها` (`:19`): `GET brand` (`:217`), `POST brand/create` (`:224`) → `برند اضافه شد`, `PUT brand/{id}` (`:241`) → `تغییرات ثبت شد`, delete via handler → `برند حذف شد`. Gated `config.statics.brand.create/.edit/.remove`.
`برچسب ها` (`:65`): `GET const/__list__tags` (`:346`), `PUT const/__list__tags {val}` (`:275`) → `برچسب ها ذخیره شد`. Gated `config.statics.tags.create/.remove`.
`اسلایدر موبایل` (`:99-102`, only when `custom.config.statics.slider`, `:347`): `GET const/slider`, `PUT const/slider {val}` (`:290`) → `اسلایدر ذخیره شد`; uses `ImageUploadInput`.
`سینی ها` (collections, `:120`): `POST ciny {page:1,limit:200}` (`:299`), `PUT ciny/{id}` / `POST ciny/create` (`:307-309`) → `عملیات انجام شد`, `DELETE ciny/{id}` (`:333`) → `حذف شد`.
#### `src/views/manage/wallet.vue`
`GET wallet/config` + `GET wallet/birthday/config` (`:137-138`); `POST wallet/config {expire, percent}` (`:101`); `POST wallet/birthday/config` (`:122`). Both → `تغییرات ثبت شد`. Gated `config.wallet`, `wallet.percent`.
#### `src/views/promotion/PromotionForm.vue` (408 LOC, backs omde + tak)
Loads in parallel (`:356-358`): `POST products {page:1,limit:500}`, `POST category {page:1,limit:500}`, `GET brand`; plus `GET query/customer` (`:369`) and, in edit mode, `GET admin/promotions/{id}` (`:378`).
Validation (`:323-325`): `!form.title` → `لطفا مقدار را وارد کنید`; missing discount percent → `لطفا مقدار را وارد کنید`.
Submit `PUT product/variant/promotion/{id}` / `POST product/variant/promotion` (`:335-337`) → `اطلاعات با موفقیت به ثبت رسید.`
#### `src/views/promotion/promotion_list.vue`
Table `admin/promotions/list` (`promotionListModel.ts:5`). Columns: `id` کد (filterNumber); `title` نام پروموشن (filterText); `promotion_type_str` نوع پروموشن; `platform` پلتفرم نمایش ; `status` وضعیت (slot `sts`); `start` تاریخ شروع (slot `startDate`); `end` تاریخ پایان (slot `endDate`); `action` عملیات (slot `op`). Delete `DELETE major/promotion/{id}` (`:129`) → `تخفیف حذف شد` / `خطا در حذف تخفیف`. Edit/remove gated on `offers.list.edit` / `offers.list.tak.edit` / `offers.list.remove` / `offers.list.tak.remove`.
#### `src/views/promotion/autopromotion.vue`
`GET autopromotion` (`:69`), `POST autopromotion` (`:55`). Validation: `err.discount_percent = 'وارد کردن درصد تخفیف اتوماتیک الزامی است'` (`:50`); success `اطلاعات با موفقیت به ثبت رسید.` (`:59`).
#### `src/views/offersIgnore/offersIgnore-add-edit.vue`
`GET ignore` (`:198`, error `خطا در دریافت اطلاعات`), `POST ignore` (`:166`) → `عملیات با موفقیت انجام شد`, `DELETE ignore/{type}` (`:184`) → `حذف شد`.
#### `src/views/city/cityList.vue` / `createCity.vue`
List table `admin/misc/cities/list` (`CityListModel.ts:8`). Columns: `title` نام شهر (filterKey `name`, filterText); `level` سطح (filterNumber, noSort — **no such field in the payload**, always blank; legacy quirk `:3-4`); `percent` درصد مشتری عمده (slot); `percent_aux` درصد کارشناس فروش (slot); `opt` عملیات.
Editing a row loads `GET cities/get-percent/{id}` + `GET cities/get-percent-aux/{id}` (`:92-93`) then `PUT cities/{id}` (`:106`) → `تغییرات ثبت شد`. Gated `cities.list.view.edit` / `cities.list.remove`; delete → `شهر حذف شد` (`:120-122`).
Create: validation `err.title = 'لطفا مقدار را وارد کنید'` (`:80`); `POST cities` (`:85`).
#### `src/views/branch/AddBranches.vue` / `FacilityList.vue` / `workshop/AddWorkshop.vue`
Branch form: `GET user/list` for the manager select (`:162`); edit `GET branch/{id}` (`:169`); submit `PUT branch/{id}` / `POST branch` (`:139-141`). Validation `err.name = 'لطفا مقدار را وارد کنید'` (`:133`).
Workshop form: validation `!form.workshop_name` → `لطفا مقدار را وارد کنید` (`:141`); `!user_phone` → `موبایل سرپرست کارگاه الزامی است` (`:144`); submit `PUT workshop/{id}` / `POST workshop` (`:151-153`).
`FacilityList.vue` is shared by `ListBranches.vue` and `ListWorkshop.vue`; the active toggle calls `GET {activePath}/{id}` (`:104`) → `تغییرات ثبت شد` / `خطا در تغییر وضعیت`.
Branch columns (`ListBranch.ts:10-15`): `name` نام, `address` آدرس, `phone` شماره تلفن (filterNumber), `user.name` مدیر شعبه (filterNumber, noSort), `op` عملیات. Workshop columns (`ListWorkshop.ts:6-9`): `name`, `address`, `phone` (filterNumber), `op`.
#### `src/views/sms/create.vue`
Recipient mode gated per rid: `sms.create.single`, `sms.create.multiple`, `sms.create.all`, `sms.create.reagent`, `sms.create.omde`; log tab gated `sms.list`.
Validation (`:86-88`): empty text → `متن پیامک الزامی است`; empty mobile → `شماره موبایل الزامی است`.
Submit `POST sms` (`:94`); success `پیامک برای ${data.sentCount} شماره ثبت شد` (`:99`); error `خطا در ارسال پیامک`.
Log table `admin/misc/sms/list` (`smsTbl.ts:6-12`): `id` شماره; `text` متن پیامک (filterKey `body`, filterText); `mobile` گیرندگان (noSort); `sent_count` تعداد; `sender` ارسال کننده; `caj` تاریخ (filterKey `createdAt`, filterDate).
#### `src/views/notify/create.vue` / `user.vue`
Create validation (`:41-42`): `!form.title` → `عنوان الزامی است`; `!form.text` → `متن پیام الزامی است`. `POST notify` (`:47`) → `نوتیفیکیشن ارسال شد`.
Received list `admin/misc/notify/list` with `noFilters()` (`notifyTbl.ts:11`); columns `id` شماره, `title` عنوان, `text` متن (noSort, key differs from legacy's `note` — `:8-9`), `opt` عملیات. Detail `GET notify/{id}` (`:73`); mark read `GET notify/read/{id}` (`:81`) → `علامت‌گذاری شد`.
#### `src/views/report/order_item.vue` / `product.vue`
`GET report/order-item` and `GET report/product`, both with `{ params: filters }` (`:76`, `:60`) — query-string filters, not the XTable POST envelope. Date filters use `DateInput.vue` (Jalali → ISO).
#### `src/views/statics/index.vue`
`GET log/list/customer` (`:82`) then per-customer `GET log/ago/{customer}` (`:71`).
#### `src/views/wallet/wallet.vue`
Table `admin/documents/wallet/list` with `noFilters()` (`walletTbl.ts:11`). Columns: `id` شماره; `customer.name` نام مشتری (filterKey `customer.fullName`); `order.id` شماره سفارش (filterText); `value` میزان شارژ (slot); `expire_at` تاریخ انقضا (noSort, slot `time`).
#### `src/views/newsletters/newsletters.vue`
Table `admin/misc/newsletter/list` (`newsletter.ts:9`). Columns: `name` نام (filterKey `email`, filterText); `mobile` شماره موبایل (filterNumber); `op` عملیات (slot `actions`).
#### `src/views/reg.vue` (PUBLIC — in `noneAuthUrl`)
Validation (`:85-87`): `!fname` → `لطفا نام را وارد کنید`; `!lname` → `لطفا نام خانوادگی را وارد کنید`; `!mobile` → `لطفا شماره تلفن همراه را وارد کنید`. Submit `POST customer/create` (`:92`) → `اطلاعات با موفقیت ثبت شد.`
#### `src/views/branch/dashboard.vue` (branch mode)
Parallel `GET get-analytics-tak` + `POST products {page:1,limit:5}` (`:75-76`).
#### `src/views/user/myData.vue`
`GET admin/users/me` (`:37`). Fallback target for the dashboard guard redirect (`src/router/index.ts:121`).
### 6.3 Export
XTable's export buttons (excel/json/print) and the fullscreen toggle were **dropped** — the legacy endpoint returning a file URL is not implemented in the new backend (`src/components/x-table/XTable.vue:8-12`). A decorative `چاپ` (print) button exists with **no handler** (`XTable.vue:32-36`). `requestData()` still sends `select`, `titles` and `report_title` (`XTable.vue:263-280`), i.e. the server-side export contract is preserved even though no client triggers it.
---
## 7. Store (Pinia)
| Store | File | State | Getters | Actions | Endpoints |
|---|---|---|---|---|---|
| `auth` | `src/stores/auth.ts` | `userPermission: string[]` (`:120`) | `token` (localStorage `tokenName`, `:123`), `user` (JSON from `userName`, `:124-132`), `isLoggedIn` (`:133-135`) | `login(payload)` (`:148-162`), `inject(payload)` (`:165-170`), `logout()` (`:177-184`) | `POST admin-login` (`:149`) |
| `users` | `src/stores/users.ts` | `cityFilters: Record<id,title>` (`:251`), `userInfo` (`:252`), `roles?: string[]` (`:255`) | `hasPermission(id)` (`:263-269`) | `setCities` (`:273`), `getCities()` (`:278-285`), `setUserInfo` (`:286`), `deleteUserInfo` (`:289`), `getRoles(roles)` → `checkParent` (`:293-295`), `deleteRoles` (`:296`) | `POST cities/list` (`:280`) |
| `cart` | `src/stores/cart.ts` | `items: CartItem[]` (`:45`) | — | `addToCart` (`:49-61`), `setItems` (persist, `:64-67`), `add` (`:69`), `remove` (`:74`), `updateItemQuantity` (`:80`), `clear` (`:85`), `init()` (`:94-123`) | `POST product/variant {products_id}` (`:102`) |
| `invoiceCart` | `src/stores/invoiceCart.ts` | `items: InvoiceCartItem[]` (`:28`) | — | `setItems` (`:32`), `add` (`:38`), + remove/clear | **none** (no rehydrate fetch, `:12-13`) |
| `layout` | `src/stores/layout.ts` | `windowWidth`, `scrollY`, `theme`, `isSidebarActive`, `sidebarCollapsed` (`:323-329`) | `isSmallScreen` (`<992`, `:333`) | `updateWindowWidth`, `updateWindowScrollY`, `toggleSidebar`, `setSidebarCollapsed` (persists `admin-panel:sidebarCollapsed`) (`:336-356`) | — |
`auth.login` (`src/stores/auth.ts:148-162`): `POST admin-login` → `users.getRoles(data.roles.map(x => x.rid))`, strips `roles` + `ut`, then `inject()` writes `tokenName`, `login='true'` (consumed once by `App.vue` for the welcome toast) and `userName` to localStorage.
`auth.logout` (`:177-184`) — **not in legacy**: clears token, user, `login`, and both `users.deleteRoles()` / `deleteUserInfo()` (fixes the cross-session permission leak).
`cart` persistence key: `isBranch() ? 'branch_cart_items' : 'admin_cart_items'` (`:16`). `cart.init()` refetches products and drops out-of-stock, non-pre-order variants (`:108`).
`invoiceCart` key: `isBranch() ? 'branch_invoice_items' : 'admin_invoice_items'` (`:17`) — deliberately different from legacy, which reused `admin_cart_items` for both and clobbered data (`:6-10`).
---
## 8. Shared utils
### 8.1 `src/utils/index.ts` (19 lines)
| Export | Value | Line |
|---|---|---|
| `isBranch()` | `import.meta.env.MODE === 'branch'` | `:12-14` |
| `tokenName` | `isBranch() ? 'branch_token' : 'token'` | `:18` |
| `userName` | `isBranch() ? 'branch_user' : 'user'` | `:19` |
### 8.2 `src/utils/filters.ts` (72 lines) — formatters (Vue 3 has no `Vue.filter`, so these are plain functions)
| Function | Behaviour | Line |
|---|---|---|
| `capitalize(value)` | Capitalizes each space-separated word; `''` when falsy | `:29-39` |
| `title(value, replacer='_')` | Capitalizes each `replacer`-separated word | `:41-51` |
| `truncate(value, limit)` | `value.substring(0, limit)` | `:53-55` |
| `priceUnit(val)` | Thousands separators + `' تومان '` (**trailing space**); returns input **unchanged** when falsy, so `0` renders `"0"` not `"0 تومان"` | `:58-63` |
| `price(val)` | Thousands separators only, regex `/\B(?=(\d{3})+(?!\d))/g` → `','`; returns input unchanged when falsy | `:66-71` |
| `jdate(val)` | `moment(val).format('jYYYY/jMM/jDD')` | `:74-79` |
| `jdateTime(val)` | `moment(val).format('jYYYY/jMM/jDD - hh:mm:ss')` — **12-hour `hh`**, legacy quirk preserved | `:82-87` |
| `none(val)` | Identity | `:89-91` |
**Gram/weight**: there is **no dedicated gram formatter**. Weights are rendered raw with a literal `گرم` suffix at each call site (e.g. `Dashboard.vue:23,50,112,116`) or via `Number(x).toFixed(2)` (`Dashboard.vue:217,222`).
**Persian↔English digit conversion**: only one implementation, inline in `src/components/input/DateInput.vue:121`:
`raw.replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)))`. There is **no shared digit-normalisation util** and **no price-input mask**.
### 8.3 Validators
No validator module exists. Every form hand-rolls a `validate()` that writes into a reactive `err` object; the recurring message is `لطفا مقدار را وارد کنید` / `مقدار را وارد نمایید` (see §6.2 per view).
### 8.4 Constants / enums with exact values
| Constant | Values | Source |
|---|---|---|
| `show_type` (product display) | `1: سایت`, `2: گوشی`, `3: سایت و گوشی` | `src/views/apps/product/productListModel.ts:20-22`; same in `AddProduct.vue:100-102` |
| Wholesale order status | `0: معلق`, `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی ` (trailing space), `5: تحویل مسئول فروش`, `6: تحویل مشتری ` (trailing space) | `src/views/order/OmdeListModel.ts:35-41` |
| Customer confirm status | `1: تایید شده`, `2: در انتظار تایید` | `src/views/order/OmdeListModel.ts:48-49`, `TaminListModel.ts:29-30` |
| Account active status | `'1': فعال`, `'-1': غیر فعال` | `src/views/customer/omde/omdeListModel.ts:56-57`, `ReagentListModel.ts:39-40` |
| Document confirm status | `0: تایید نشده`, `1: تایید شده` | `src/views/finical/list_model.ts:27-28` |
| Variant active tag | `فعال` / `غیر فعال` (green/red) | `src/views/apps/product/AddProduct.vue:186-188` |
| Variant pre-order tag | `بله` / `خیر` (blue/none) | `src/views/apps/product/AddProduct.vue:191-193` |
| Dashboard tag colours | `success→green`, `warning→orange`, `danger→red`, `primary→blue` | `src/views/Dashboard.vue:209-211` |
| Dashboard chart palette | `COLOR_PRIMARY '#1c2d56'`, `COLOR_SUCCESS '#28c76f'`, `COLOR_WARNING '#ff9f43'`, `COLOR_DANGER '#ea5455'` | `src/views/Dashboard.vue:190-193` |
| `BaseFormModal` widths | `sm:520, md:720, lg:900, xl:1140` | `src/components/common/BaseFormModal.vue:230-235` |
| OMDE list sources | 5 url/title pairs | `src/views/customer/omde/omdeListModel.ts:11-19` |
| `FilterType` | `'default' \| 'switch' \| 'query' \| 'date' \| 'number' \| 'select'` | `src/components/x-table/model.ts:15` |
| Sidebar persist key | `'admin-panel:sidebarCollapsed'` | `src/stores/layout.ts:308` |
| Small-screen breakpoint | `992` | `src/stores/layout.ts:333` |
---
## 9. Components
| Component | File | Role / API |
|---|---|---|
| **XTable** | `src/components/x-table/XTable.vue` | The one surviving table wrapper (legacy had 4 forks, `model.ts:9-12`). Props `model: XTbl`, `has-selection`, `v-model` = selected rows. Contract: `POST model.url` with `{search, filters, total, limit, page, order, select, titles, report_title}` (`:263-280`), response `{rows, query:{total,page,limit}}` (`:289-294`). Exposes `fetch()`. Toolbar: search (`:17-25`), filter bar with badge count (`:30-37`), column show/hide dropdown (`:41-53`), reload (`:55-60`), decorative print button with no handler (`:32-36`). Pagination `{current, pageSize, total, showSizeChanger:true}` unless `_hidePagination` (`:251-259`). Sorting via `onTableChange` → `sort` + `sortDirection` (`asc`/`desc`, `:331-337`). `addFilterRow()` keys rows with a random float (`:315-317`). |
| **XTbl / Xtc** | `src/components/x-table/model.ts` | Fluent column API. `Xtc(key,title)` with `.filterText()` `.filterSwitch()` `.filterQuery(action)` `.filterDate()` `.filterNumber()` `.filterSelect(itemsMap?)` `.filterKey(k)` `.renderSlot(name)` `.noSort()` `.hidden()` `.setWidth(w)` (`:55-129`). `XTbl(url,title)` with `.add(col)` `.addDefaultFilter()` `.hidePagination()` `.hideSearch()` `.noFilters()` `.getSortFields()` (`:139-188`). `filterSelect` accepts an **object map** value→label and is legitimately callable with no argument (`:98-112`). Legacy's `hideSearch()` bug (set `_hidePageination`) is fixed here (`:169-178`). |
| **ImageUploadInput** | `src/components/upload/ImageUploadInput.vue` | Single-image uploader (`a-upload`, `list-type="picture-card"`, `max-count=1`, `accept="image/*"`). `POST file/create` as FormData with `file` + optional `purpose` (`:88-99`). `v-model` is the stored **URL**; also emits `ok` with the whole response. Watches `modelValue` to keep the thumbnail in sync (`:74-84`). Preview modal (`:32-34`). Error `بارگذاری فایل ناموفق بود` (`:103`). Replaces legacy's four near-identical uploaders (`:6-9`). |
| **DateInput** | `src/components/input/DateInput.vue` | Jalali date input. Displays `jYYYY/jMM/jDD`, emits ISO `YYYY-MM-DD` (`:126`). Normalises Persian digits before parsing (`:121`). Invalid input → inline hint `تاریخ معتبر نیست` (`:128`), styled `#ea5455` (`:135`). Replaces `vue-persian-datetime-picker` (no Vue 3 build, `:56-60`). Placeholder default `مثال: ۱۴۰۳/۰۵/۱۲` (`:84`). |
| **BaseFormModal** | `src/components/common/BaseFormModal.vue` | Standard add/edit modal shell. Props `open`, `id` (presence switches title to `editTitle`), `addTitle`, `editTitle`, `loading`, `submitting`, `width` (`sm\|md\|lg\|xl`), `okText` (default `ثبت اطلاعات`). Cancel text `انصراف`, `mask-closable: false`. Emits `update:open`, `submit`, `cancel`. Replaced 16 hand-rolled modals with inconsistent widths (`:174-176`). |
| **ListPageHeader** | `src/components/common/ListPageHeader.vue` | Permission-gated "add" button for list pages: `v-if="!rid || hasPermission(rid)"` (`:146`). Props `rid`, `addLabel` (default `افزودن`). Emits `add`. Rendered inside XTable's `#add` slot. |
| **TooltipBtn** | `src/components/tooltip-btn/TooltipBtn.vue` | Row-action icon button. Props `title`, `popTitle`, `icon`, `iconClass` (default `w-5 h-5`). With `popTitle` wraps in `a-popconfirm` (بله/خیر, `:259-266`); without, fires immediately. Icon resolved from a legacy Feather name via `resolveIcon` (`:297`), falling back to `UnorderedListOutlined`. |
| **StatisticsCardLine** | `src/components/statistics-cards/StatisticsCardLine.vue` | KPI tile: `icon`, `color`, `statistic`, `statistic-title`. The legacy sparkline branch is **not ported** — every live caller passed `hideChart` (`:307-312`). Used 6× on the Dashboard. |
| **icons.ts** | `src/components/icons.ts` | Legacy Feather-name → ant-design icon map, shared by TooltipBtn and the sidebar (`:1-6`). Exports `ICONS` and `resolveIcon`. |
| **useDeleteHandler** | `src/composables/useDeleteHandler.ts` | Shared delete composable (not a component). `remove(url, {method?: 'delete'\|'post'})`; success → `message[successType](successMessage)` then `onSuccess()`; failure → `Modal.error({ title: server message ?? fallback, okText:'بستن' })` (`:43-46`). Defaults: `با موفقیت حذف شد`, `success`, `خطا در حذف`. Used by 6 list views. |
| **Layout** | `src/layouts/main/Main.vue` (91), `components/TheNavbar.vue` (223), `components/VerticalNavMenu.vue` (192), `components/NavMenuNode.vue` (45), `full-page/FullPage.vue` (6) | Main shell = navbar + vertical nav + `<router-view>`; FullPage is a bare `<router-view>` for login/error pages. |
**Absent**: there is **no price-input component with a mask or digit conversion**. Price/number fields use plain `a-input-number` (e.g. `AddProduct.vue:212,215,218`). Digit conversion exists only inside `DateInput.vue`.
---
## 10. PROTECTED zones — precise description
### (a) Dashboard — `src/views/Dashboard.vue` (327 lines)
**Route**: two paths both mount this one file — `/dashboard/ecommerce` (`dashboard-ecommerce`, meta rule `admin`, role `dashboard.view`, `src/router/admin.ts:54-61`) and `/dashboard/analytics` (`dashboard-analytics`, rule `editor`, same role, `:36-43`). `/` redirects to `/dashboard/ecommerce` (`:27-30`). This **merges legacy's two dashboards** (`آمار عمده فروشی` + `آمار تک فروشی`, two rids) into one screen behind one rid (`Dashboard.vue:2-10`). A user lacking `dashboard.view` is redirected to `{name:'my-data'}` rather than the 403 page (`src/router/index.ts:120-122`).
**Data**: a single `GET dashboard/overview` in `onMounted` (`:301`) → `admin/dashboard/overview` (`endpoints.ts:146`). `Object.assign(data, res)`, then two **client-side sorts** preserved from legacy: `data.reagent` desc by `grams_sell` (`:304-306`) and `data.lastOrder` desc by `w` (`:307-309`). Wrapped in `a-spin :spinning="loading"` (`:11`); errors only `console.error` (`:311`).
**Reactive shape** (`:172-186`): `customerCount`, `allgram`, `ordercount`, `reagent[]`, `lastOrder[]`, `sumLastOrder`, `sumLastOrderMonth`, `ordersTrend[]`, `statusBreakdown[]`, `channelBreakdown[]`, `topProducts[]`, `goldPricePerGram`, `totalCustomerDebtGrams`. Interfaces: `OrdersTrendPoint {date,total,retail,wholesale}` (`:149-154`), `StatusBreakdownRow {status,label,count}` (`:155-159`), `ChannelBreakdownRow {channel,label,count,grams}` (`:160-165`), `TopProductRow {title,weight,qty}` (`:166-170`).
**Layout, top to bottom**:
1. **KPI row** — `.grid-4`, four `StatisticsCardLine` (`:13-38`):
| icon | color | statistic | title | line |
|---|---|---|---|---|
| UsersIcon | warning | `${data.customerCount ?? 0} نفر` | مشتریان جدید (۳۰ روز گذشته) | `:14-19` |
| ShoppingBagIcon | primary | `${data.allgram ?? 0} گرم` | طلای فروش رفته (۳۰ روز گذشته) | `:20-25` |
| ShoppingBagIcon | success | `${data.ordercount ?? 0} سفارش` | تعداد سفارشات (۳۰ روز گذشته) | `:26-31` |
| DollarSignIcon | danger | `goldPriceLabel` | نرخ هر گرم طلای ۱۸ عیار | `:32-37` |
2. **Second KPI row** — `.grid-2.mt-4`, two tiles (`:40-53`): BarChartIcon/primary `${data.sumLastOrder ?? 0} سفارش` "تعداد سفارشات تحویل‌شده (۳۰ روز گذشته)"; CreditCardIcon/warning `${totalDebtLabel} گرم` "مجموع بدهی طلای مشتریان".
3. **`روند سفارش‌ها (۳۰ روز گذشته)`** — `a-card` + apexchart **type `area`, height 300** (`:56-63`). Two series: `تک فروشی` = `ordersTrend.map(d => d.retail)`, `عمده فروشی` = `d.wholesale` (`:226-232`). Options (`:233-252`): colors `[COLOR_PRIMARY, COLOR_SUCCESS]`, toolbar hidden, `dataLabels` off, `stroke {curve:'smooth', width:2}`, gradient fill `opacityFrom .35 / opacityTo .05`, legend top-right, x-axis categories `jdate(d.date)` with **`rotate:-45, rotateAlways:true, hideOverlappingLabels:true, fontSize 12px, fontWeight 500`** (added deliberately because 30 daily labels overlapped, `:242-249`), `tooltip {shared:true}`.
4. **`.grid-2.mt-4`** — two **donut** charts, height 300 (`:65-73`):
- `وضعیت سفارشات (۳۰ روز گذشته)`: series `statusBreakdown.map(s => s.count)`, labels `s.label`, an **8-entry colour array** `[WARNING, PRIMARY, PRIMARY, WARNING, WARNING, SUCCESS, SUCCESS, DANGER]` (`:259`), legend bottom, dataLabels on.
- `تفکیک عمده / تک (۳۰ روز گذشته)`: series `channelBreakdown.map(c => c.count)`, labels `c.label`, colors `[PRIMARY, SUCCESS]`, legend bottom, dataLabels on (`:265-272`).
5. **`.grid-2.mt-4`** — two cards (`:75-104`):
- `عملکرد کارشناسان فروش`: **horizontal bar** (`plotOptions.bar {horizontal:true, borderRadius:4}`, `:280`), height 280, rendered only `v-if="agentChartSeries[0]?.data.length"` (`:78`); series `گرم فروخته‌شده` from `reagent[].grams_sell`, x categories `reagent[].name` (`:275-284`). **Always** followed by an `a-table` (`size small`, `pagination:false`, `row-key="id"`) with columns کارشناس فروش (`name`) / میزان فروش (گرم) (`grams_sell`) / تعداد سفارش (`order_count`) (`:84-91`, `:195-199`).
- `محصولات پرفروش (۳۰ روز گذشته)`: **vertical bar**, height 280, `columnWidth '55%'`, `borderRadius 4`, color `[COLOR_SUCCESS]`, dataLabels off, categories `topProducts[].title`, series `وزن فروخته‌شده (گرم)` from `topProducts[].weight` (`:287-296`); falls back to `<a-empty description="داده‌ای برای نمایش وجود ندارد" />` (`:102`).
6. **`میزان فروش`** — `a-descriptions bordered size=small :column=3` (`:106-118`): تعداد سفارش (۳۰ روز) = `sumLastOrder`; مجموع وزن (۳۰ روز) = `sumLastOrderMonth` + `گرم`; میانگین هر سفارش = `avgWeightPerOrder` + `گرم`.
7. **`سفارشات هفته جاری`** — `a-table` over `data.lastOrder`, `size small`, `pagination {pageSize:10}`, `row-key="id"` (`:120-137`). Columns (`:201-207`): شماره سفارش (`name`), مشتری (`customer`), وزن (`w`), وضعیت (`sts_str`), تاریخ (`caj`). `bodyCell` overrides: `sts_str` → `<a-tag :color="tagColor(record.statusColor)">` (`:129-131`); `caj` → `jdate(record.caj)` (`:132-134`).
**Computed** (`:213-223`): `goldPriceLabel` = `price(Math.round(Number(goldPricePerGram))) + ' تومان'`, or `'—'` when falsy; `totalDebtLabel` = `price(Number(totalCustomerDebtGrams ?? 0).toFixed(2))`; `avgWeightPerOrder` = `sumLastOrderMonth / sumLastOrder` to 2 dp, `'0'` when count is 0.
`tagColor(c)` maps `{success:'green', warning:'orange', danger:'red', primary:'blue'}`, default `''` (`:209-211`).
Scoped styles only define `.mt-4` (1rem) and `.mt-3` (.75rem); `.grid-2`/`.grid-3`/`.grid-4` come from the global `utilities.less` (`:318-326`).
**No filters, no date-range picker, no export, no drill-down links, no refresh button** on this screen — one fetch on mount.
### (b) Variants section inside Create/Edit Product — `src/views/apps/product/AddProduct.vue`
`AddProduct.vue` (579 lines) backs **both** create (`/apps/product/add`, role `product.create`) and edit (`/apps/product/edit/:id/:variant_id?`, role `product.list.edit`) — `EditProduct.vue` (17 lines) is only a wrapper.
**Card gate**: the whole `تنوع ها` card renders only with `product.list.variants` (`:151`). The header `افزودن تنوع` button needs `product.list.variants.add` (`:155-161`).
**Variant table** (`:165-196`, `row-key="__key"`, `:pagination="false"`, `size="small"`). Columns (`:363-372`):
| title | dataIndex | rendering |
|---|---|---|
| تصویر | `image` | `<img :src="record.image_url" class="variant-thumb">` (40×40, cover, radius 4) or a dashed `—` placeholder (`:173-176`, `:520-533`) |
| عنوان | `title` | plain |
| وزن | `grams` | plain |
| اجرت درصدی | `percentage_pay` | plain |
| تعداد | `qty` | plain |
| فعال | `is_active` | `<a-tag :color="green\|red">` `فعال` / `غیر فعال` (`:185-189`) |
| ` پیش سفارش` (leading space) | `is_pre_order` | `<a-tag :color="blue\|''">` `بله` / `خیر` (`:190-194`) |
| عملیات | `op` | ویرایش (`product.list.variants.edit`, `EditOutlined`) and حذف (`product.list.variants.remove`, `DeleteOutlined` → `variants.splice(index,1)`, **no confirm dialog**) (`:177-184`) |
**Variant modal** (`:206-270`), title `افزودن تنوع` when `editingIndex === null`, otherwise `ویرایش تنوع`:
| Field | Control | Line |
|---|---|---|
| عنوان | `a-input` | `:208-210` |
| وزن | `a-input-number` `:step="0.001"`, full width | `:211-213` |
| اجرت درصدی | `a-input-number` `:step="0.1"`, full width | `:214-216` |
| تعداد | `a-input-number`, full width | `:217-219` |
| **تصویر تنوع** | **gallery picker — the extra option** | `:230-257` |
| فعال | `a-switch` | `:259-261` |
| ` پیش سفارش` (leading space) | `a-switch` | `:262-264` |
Footer: `انصراف` (closes) and `ثبت` (`saveVariant`) (`:266-269`).
**⭐ THE EXTRA / NEW OPTION — `تصویر تنوع` (variant image picker)**, `:221-257` + `:315-361` + `:519-568`:
- It is **not an uploader**. It is a **picker over the product's already-saved gallery**: the variant points at the same `ProductImage` row the gallery shows, so one photo can back several weights (`:222-224`).
- Source data is `gallery = ref<GalleryImage[]>([])` where `GalleryImage {id, url, role, sortOrder}` (`:318-324`), populated **only in edit mode** from `data.gallery` on `GET product/show/{id}` (`:487`). On create it is empty, because uploaded files have no `ProductImage` rows (hence no ids) until the first save (`:226-228`, `:315-317`).
- UI when `gallery.length` (`:231-250`): a `.variant-picker` flex-wrap row of 64×64 buttons. The **first** button is a `بدون تصویر` ("no image") tile calling `pickVariantImage(null)`, highlighted when `!variantForm.image_id` (`:232-239`). Then one `<img>` button per gallery image, highlighted (`.is-on`, blue `#1890ff` 2px border, `:556-558`) when `variantForm.image_id === img.id`.
- UI when the gallery is empty (`:251-256`): an `a-alert type="info" show-icon` whose message is **context-dependent** — `برای این محصول تصویری ثبت نشده است.` in edit mode (`id` truthy) vs `ابتدا محصول را ذخیره کنید تا بتوانید تصویر تنوع را انتخاب کنید.` on create.
- `pickVariantImage(img)` sets both `variantForm.image_id = img?.id ?? null` and `variantForm.image_url = img?.url ?? null` (`:358-361`). `image_id` is what gets **posted**; `image_url` is carried purely so the table can draw the thumbnail without a lookup (`:336-340`).
**Variant row type** `VariantRow` (`:327-341`): `__key` (client key), `id?`, `title`, `grams?`, `percentage_pay?`, `qty?`, `is_active`, `is_pre_order`, `image_id?`, `image_url?`.
`variantForm` defaults (`:346-356`): `title:''`, `grams:undefined`, `percentage_pay:undefined`, `qty:0`, `is_active:true`, `is_pre_order:false`, `image_id:null`, `image_url:null`.
`showModal(record?, index?)` (`:374-394`): editing copies the record via `Object.assign`; adding resets and mints `__key = 'v-' + Date.now() + '-' + random base36 slice` (`:381`).
`saveVariant()` (`:396-408`): the **only** variant validation is `if (!variantForm.title) message.error('عنوان تنوع الزامی است')` (`:397-400`); otherwise pushes or replaces at `editingIndex` and closes.
**Persistence** — variants are edited **in memory** and saved with the product, never through a per-variant endpoint. `handleSubmit()` (`:416-448`) builds `payload = {...form, cats: form.category_id ? [form.category_id] : [], images: gallerySite, images_mobile: galleryMobile, variants: variants.map(v => ({id, title, grams, percentage_pay, qty, is_active, is_pre_order, image_id: v.image_id ?? null}))}` (`:420-435`) — note `image_url` and `__key` are **stripped** before posting. Then `PUT product/update/{id}` (edit) or `POST product/create` (`:436-440`), success `اطلاعات با موفقیت به ثبت رسید.` and `router.push('/apps/product/list')`.
On load in edit mode, variants come from `data.variants_all` mapped with `__key: v.id ?? 'v-'+i`, `grams: Number(v.grams)`, `percentage_pay: Number(v.percentage_pay)`, `image_id: v.image_id ?? null`, `image_url: v.image?.url ?? null` (`:488-499`).
**Rest of the product form (context, also in the same file)**: image card with **4 slots** — تصویر شاخص (`indicator_id`, purpose `product.indicator`), تصویر شاخص موبایل (`mobile_pic`, `product.indicator_mobile`), گالری تصاویر سایت (`gallerySite[]`, `product.gallery`), گالری تصاویر موبایل (`galleryMobile[]`, `product.gallery_mobile`) (`:35-82`). Basic-info card: کد محصول, نام محصول (the only validated field → `لطفا مقدار را وارد کنید`, `:412`), نوع نمایش (1/2/3), دسته بندی, برند, سینی ها (multiple), شرح محصول (سایت) textarea, نمایش محصول switch (`:84-148`). Lookups on mount: `GET brand`, `POST category`, `GET admin/products/collections` (`:452-456`); category titles get legacy's ` (${type_str}) ` suffix (`:460-463`). Fields with no schema column are deliberately **not rendered**: `delivery_type`, `message_long_delivery`, `offer_day`, `has_warranty`, `is_exist`, `tags`, and the variant colour/rial-wage fields (`:10-13`); legacy's per-gallery-image `imageBody` caption and the 5th "پادکست" audio slot are also dropped (`:27-30`).
---
## 11. Env / config
### 11.1 `import.meta.env` usages (only 4 runtime reads)
| Key | Use | Line |
|---|---|---|
| `VITE_API_BASE_URL` | axios `baseURL`, falls back to `'/api/'` | `src/api/client.ts:11` |
| `BASE_URL` | 401 redirect target `${BASE_URL}auth/login` | `src/api/client.ts:77` |
| `BASE_URL` | `createWebHistory(...)` | `src/router/index.ts:10` |
| `MODE` | `isBranch()` → `MODE === 'branch'` | `src/utils/index.ts:13` |
No other `VITE_*` variable is read anywhere in `src`.
### 11.2 `vite.config.ts` (70 lines)
`defineConfig(({ mode }) => ...)`; `const isBranch = mode === 'branch'` (`:10`).
| Setting | Default mode | `branch` mode | Line |
|---|---|---|---|
| `base` | `'/'` | `'/branch/'` | `:25` |
| `build.outDir` | `'dist'` | `'branchDist'` | `:51` |
| `build.emptyOutDir` | `true` | `true` | `:52` |
| `build.sourcemap` | `false` | `false` | `:53` |
- `envDir` points at the **repo root** (`../..`), so the app has no `.env` of its own (`:19`, `:12-13`).
- `loadEnv(mode, <repo root>, '')` reads `ADMIN_PANEL_PORT` (default 5180) and `API_PORT` (default 3000) (`:14-16`). Confirmed in the root `.env`: `API_PORT=3000` (line 24), `ADMIN_PANEL_PORT=5180` (line 25).
- Alias `@` → `./src` (`:27-31`).
- `css.preprocessorOptions.less.javascriptEnabled: true` (`:32-42`); ant-design-vue 4 dropped LESS `modifyVars`, so theming now lives in `App.vue`'s `<a-config-provider :theme>` (`:35-38`) — LESS only compiles the app's own `farsi.less` / `utilities.less`.
- `server`: `host '0.0.0.0'`, `port`, **`strictPort: true`** (fail loudly rather than drift to another port, `:58-60`), proxy `/api` → `http://localhost:${apiPort}` and `/uploads` → same (needed in dev because uploaded `ProductImage.url` files are served by the API's static handler; production nginx already proxies them) (`:61-67`).
**`branch` mode summary** — it is a *different application variant*, not a theme:
`--mode branch` → `isBranch()` true → different **routes** (`branchRoute`/`fullPageBranchRoute` instead of `adminRoute`, `src/router/index.ts:20,27`), different **menu** (`branchMenu`, `navMenuItems.ts:637-639`), different **localStorage keys** (`branch_token`/`branch_user`, `src/utils/index.ts:18-19`; `branch_cart_items`, `src/stores/cart.ts:16`; `branch_invoice_items`, `src/stores/invoiceCart.ts:17`), a different **login page** (`views/branch/login.vue`), different **base path** and **output dir**. `ProductList.vue:32` additionally hides all row actions in branch mode.
Other config files: `tsconfig.json`, `Dockerfile` (`COPY dist /usr/share/nginx/html`), `nginx.conf` (proxies `/api` and `/uploads`), `.dockerignore`, `.gitignore`, `public/`.