Files
tehran_taj/docs/parity/_inv-tgt-admin.md
s.esmaeiliandClaude Opus 5 71192ba2ba parity(docs): Phase 0 inventory, mapping and draft PROTECTED list
Read-only inventory of all 7 projects (3 reference + 4 target) produced by
parallel subagents, every claim cited path:line. No source code changed.

- 00-INVENTORY.md  consolidated mapping: stack, backend, admin, user apps
- PROTECTED.md     draft protected list (awaiting user confirmation)
- ISSUES.md        26 logged issues (security, money/stock, validation, dead code)
- CHANGELOG.md     change log scaffold
- PROGRESS.md      resumable state + Q1-Q5 blocking Phase 1
- _inv-*.md        7 raw per-project inventories (8111 lines)

Key finding: the reference is a gold-jewellery B2B/B2C e-commerce system, not a
gold bank. Buy/sell, deposit/withdraw, transfer, KYC levels, payment callbacks,
cron and websockets do not exist in it, so they cannot be parity gaps.

[ref: henza_server, henza_admin, henza_pwa]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 18:09:46 +03:30

1262 lines
117 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`.