Files
tehran_taj/docs/parity/_inv-ref-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

1706 lines
127 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 — henza_admin (reference admin)
> Read-only, evidence-based inventory. All `path:line` are relative to `henza_admin/`.
> Source read at: repo root `E:\gold bank\henza_admin`.
---
## 1. Stack & Versions
All from `package.json`.
| Concern | Package | Version | Line |
|---|---|---|---|
| Framework | `vue` | `^2.6.10` | package.json:47 |
| Router | `vue-router` | `^3.0.6` | package.json:65 |
| Store | `vuex` | `^3.1.1` | package.json:77 |
| UI lib (primary, tables/forms/modals) | `ant-design-vue` | `^1.6.0` | package.json:8 |
| UI lib (secondary, layout/theme/notify) | `vuesax` | `3.11.13` | package.json:76 |
| HTTP client | `axios` | `^0.19.0` | package.json:11 |
| Date (Jalali) | `moment-jalaali` | `^0.9.2` | package.json:29 |
| Date picker (Jalali) | `vue-persian-datetime-picker` | `^2.4.1` | package.json:56 |
| Charts | `apexcharts` `^3.36.0` + `vue-apexcharts` `^1.6.2` | | package.json:7, 48 |
| Charts (alt) | `chart.js` `^2.8.0` + `vue-chartjs` `^3.4.2` | | package.json:12, 51 |
| Charts (alt) | `echarts` `^4.2.1` + `vue-echarts` `^4.0.3` | | package.json:15, 54 |
| Grid (unused legacy) | `ag-grid-community` / `ag-grid-vue` | `^28.1.1` | package.json:4-5 |
| Validation (legacy) | `vee-validate` | `^2.2.8` | package.json:46 — registered main.js:84-85; used only in `views/auth/Login.vue`. Business forms use ant-design `v-decorator` rules. |
| ACL | `vue-acl` | `4.0.7` | package.json:49 |
| Excel export | `xlsx` `^0.15.0`, `file-saver` `2.0.1` | | package.json:78, 16 |
| i18n | `vue-i18n` | `^8.11.2` | package.json:58 |
| CSS | `tailwindcss` `^1.0.1` + `postcss-rtl` `1.5.0` + `sass` | | package.json:84, 32 |
| Firebase (legacy, auth store only) | `firebase` | `^9.6.7` | package.json:17 |
| Node | `node:^14--npm:6` | | package.json:3 |
| App version | `5.0.1` | | package.json:3 |
**Build modes** — `vue.config.js:3-4`: `publicPath` = `/branch/` when `NODE_ENV=='branch'`, else `admin/`; `outputDir` = `branchDist/export` vs `dist/export`. LESS `modifyVars`: `primary-color:#1c2d56`, `secondary-color:#836447`, `third-color:#f5cb55`, `border-radius-base:6px` (vue.config.js:15-20).
---
## 2. HTTP Layer
### 2.1 `src/axios.js` (primary instance — used everywhere)
| Aspect | Value | Evidence |
|---|---|---|
| Base URL | `https://henzagold.com/oapi/admin/` (hard-coded, exported as `baseURL`) | src/axios.js:5 |
| Token storage key | `branch_token` if `isBranch()` else `token` | src/axios.js:6 |
| Default headers | `Content-Type: application/json`, `Accept: application/json` | src/axios.js:7-11 |
| Initial auth header | `Authorization: Bearer <localStorage[tokenName]>` set at module load if token exists | src/axios.js:13-17 |
| Request interceptor | Overwrites `config.headers["Authorization"] = "Bearer " + localStorage.getItem(tokenName)` on **every** request | src/axios.js:24-32 |
| Request error handler | `Promise.reject(error)` — **not returned** (bug: interceptor returns `undefined`) | src/axios.js:29-31 |
| Response interceptor | **None on this instance** | src/axios.js (absent) |
| Exports | `default ax`, named `baseURL`, `token` | src/axios.js:5, 33-34 |
| Registered as | `Vue.prototype.$http = axios` | src/main.js:19-20 |
### 2.2 `src/http/*` (Vuexy template leftovers — DEAD CODE)
| File | Content | Wired in? |
|---|---|---|
| `src/http/axios/index.js:1-3` | re-exports `src/axios.js` | yes (by jwt module) |
| `src/http/requests/index.js:1` | `import './auth'` | **No** — `import './http/requests'` is commented out at src/main.js:22-23 |
| `src/http/requests/auth/index.js:1` | `import './jwt/index.js'` | dead |
| `src/http/requests/auth/jwt/index.js` | see below | dead |
`src/http/requests/auth/jwt/index.js` — **never invoked** (`init()` has no caller):
- Response interceptor on 401 (src/http/requests/auth/jwt/index.js:26-43): single-flight refresh via `isAlreadyFetchingAccessToken` flag + `subscribers` queue (lines 5-14); dispatches `auth/fetchAccessToken`; retries original request with new bearer.
- `login(email,pwd)` → `POST /api/auth/login` (line 48)
- `registerUser(name,email,pwd)` → `POST /api/auth/register` (line 54)
- `refreshToken()` → `POST /api/auth/refresh-token` with `{accessToken: localStorage.accessToKen}` (line 61 — note typo `accessToKen`)
- `store/auth/moduleAuthActions.js:357-361` `fetchAccessToken` wraps `jwt.refreshToken()` — also dead (real auth module is `store/auth/moduleAuth.js`).
**Net effect: there is NO refresh-token flow and NO response interceptor in the live app.** 401s bubble to each caller's `try/catch`.
### 2.3 Error handling / toasts
Two mechanisms coexist:
| Mechanism | Definition | Usage |
|---|---|---|
| ant-design `this.$message.*` | global via `Vue.use(Antd)` (src/main.js:3,16) | e.g. `this.$message.info('ویرایش موفق آمیز')` / `this.$message.error('خطا در ویرایش')` — src/views/order/List.vue:262,265,273,276 |
| Vuesax notify wrapper plugin | `src/notify.js` (`Vue.use(notify)` at src/main.js:107-108) | 5 prototype methods below |
`src/notify.js` methods:
| Method | Title | Default text | Color | Line |
|---|---|---|---|---|
| `$ntSetError(msg)` | `خطا در ثبت اطلاعات` | `اطلاعات ثبت نگردید.` | `danger` | notify.js:4-10 |
| `$ntSet(msg)` | `اطلاعات ثبت شد.` | `اطلاعات با موفقیت به ثبت رسید` | `primary` | notify.js:12-18 |
| `$ntNoTitle(msg)` | — | `اطلاعات با موفقیت به ثبت رسید` | `primary` | notify.js:19-24 |
| `$ntRemove(msg)` | `اطلاعات با موفقیت حذف شد` | `''` | `warning` | notify.js:27-33 |
| `$ntGetError(msg)` | `خطا در ثبت اطلاعات` | `در اطلاعات دریافت شده خطایی رخ داده ` | `danger` | notify.js:35-41 |
---
## 3. Router
`src/router.js` builds the tree; route arrays live in `src/router/admin.js` (admin build) and `src/router/branch.js` (branch build). `src/router/{apps,category,customer,dashboard,full,manage,order,product,temp}.js` exist but are **not imported** by `src/router.js` (only `./router/admin` and `./router/branch` are — src/router.js:6,8) → dead.
### 3.1 Router config
| Item | Value | Line |
|---|---|---|
| mode | `history` | router.js:15 |
| base | `process.env.BASE_URL` | router.js:16 |
| scrollBehavior | always `{x:0,y:0}` | router.js:17-19 |
| Main layout | `layouts/main/Main.vue`, children = `branchRoute` or `adminRoute` per `isBranch()` | router.js:25-28 |
| Full-page layout | `layouts/full-page/FullPage.vue`, children = `fullPageBranchRoute`/`fullPageAdminRoute` + 4 error pages | router.js:32-138 |
| catch-all | `*` → redirect `/pages/error-404` | router.js:140-143 |
| afterEach | hides `#loading-bg` | router.js:147-153 |
### 3.2 Guard (`router.beforeEach`) — src/router.js:184-199
1. Whitelist `noneAuthUrl = ['/auth/login','/auth/forgot-password','/auth/register','/reg']` → `next()` (router.js:156-161, 185-187).
2. Read token from `localStorage[isBranch()?'branch_token':'token']` (router.js:188).
3. If no token → `router.push('/auth/login')` (router.js:198).
4. If token and module-level `roles` is unset → `await getPermission()` (router.js:190-191).
5. `getPermission()` (router.js:163-182): if `store.state.auth.token && store.state.auth.user` and no cached roles → `GET users/show`, dispatch `users/getRoles(data.permission)`, delete `permission`+`ut` from payload, write `localStorage[token|branch_token] = data.api_token` and `localStorage[user|branch_user] = JSON.stringify(data)`.
6. If `to.meta.role` exists and `!roles.includes(to.meta.role)` → redirect to route name `page-not-authorized` (router.js:193-195).
Note: `roles` is a **module-level** variable cached for the page lifetime (router.js:162).
### 3.3 Admin routes — `src/router/admin.js` (`adminRoute`, main layout)
`meta.rule` is the vue-acl rule; `meta.role` is the real permission gate; `meta.parent` / `meta.no_scroll` are layout hints; `meta.pageTitle` + `meta.breadcrumb` drive the header.
| path | name | component file | meta.role | meta.rule | meta.pageTitle | other meta | line |
|---|---|---|---|---|---|---|---|
| `/` | `dashboard` | *(redirect → `/my-data`)* | — | — | — | — | admin.js:3-5 |
| `/dashboard/analytics` | — | `views/DashboardAnalytics.vue` | `dashboard.tak` | editor | — | — | admin.js:8-15 |
| `/my-data` | `my-data` | `views/user/myData.vue` | *(none)* | editor | — | — | admin.js:17-24 |
| `/dashboard/ecommerce` | — | `views/DashboardECommerce.vue` | `dashboard.omde` | admin | — | — | admin.js:26-33 |
| `/config/manage/const` | — | `views/manage/Const.vue` | `config.statics` | editor | مدیریت ثابت ها | parent `manage`, no_scroll | admin.js:34-54 |
| `/config/manage/wallet` | — | `views/manage/wallet.vue` | `config.wallet` | editor | مدیریت کیف پول | parent `manage`, no_scroll | admin.js:55-75 |
| `/config/manage/report` | — | `views/manage/report.vue` | `config.report` | editor | گزارش | parent `manage`, no_scroll | admin.js:76-96 |
| `/apps/product/` | `product` | *(redirect → `/apps/product/list`)* | — | — | — | — | admin.js:98-102 |
| `/product/change/percent` | — | `views/apps/product/ChangePercent.vue` | `product.percent` | editor | — | parent `product`, no_scroll | admin.js:103-112 |
| `/apps/product/add` | — | `views/apps/product/AddProduct.vue` | `product.create` | editor | — | parent `product`, no_scroll | admin.js:113-122 |
| `/apps/product/edit/:id/:variant_id?` | `product-edit` | `views/apps/product/EditProduct.vue` | `product.list.edit` | editor | — | parent `product`, no_scroll | admin.js:123-133 |
| `/apps/product/list` | `product-list` | `views/apps/product/ProductList.vue` | `product.list` | editor | — | parent `product`, no_scroll | admin.js:134-144 |
| `/category` | `category` | *(redirect → `/category/list`)* | — | — | — | — | admin.js:146-150 |
| `/category/add/:id?` | `category-add` | `views/apps/category/Add.vue` | `collection.create` | editor | — | parent `category`, no_scroll | admin.js:151-161 |
| `/category/edit/:id` | `category-edit` | `views/apps/category/Edit.vue` | `collection.list.edit` | editor | — | parent `category`, no_scroll | admin.js:162-172 |
| `/category/list` | `category-list` | `views/apps/category/List.vue` | `collection.list` | editor | — | parent `category`, no_scroll | admin.js:173-183 |
| `/add/findoc` | — | `views/finical/addFinDoc` | `document.create` | editor | — | — | admin.js:185-192 |
| `/edit/findoc/:id` | — | `views/finical/addFinDoc` | `document.list.edit` | editor | — | — | admin.js:194-201 |
| `/order` | `order` | *(redirect → `/order/list`)* | — | — | — | — | admin.js:204-208 |
| `/order/list` | — | `views/order/List.vue` | `order.list` | editor | — | parent `order`, no_scroll | admin.js:209-218 |
| `/order/omde-list` | — | `views/order/OmdeList.vue` | `order.omde` | editor | — | parent `order`, no_scroll | admin.js:219-228 |
| `/order/make` | — | `views/order/MakeOrder.vue` | `order.makeOrder` | editor | — | parent `order`, no_scroll | admin.js:229-238 |
| `/order/create` | — | `views/order/create.vue` | `order.create` | editor | — | parent `order`, no_scroll | admin.js:239-248 |
| `/order/tamin-list` | — | `views/order/TaminList.vue` | `order.tamin` | editor | — | parent `order`, no_scroll | admin.js:249-258 |
| `major/promotion/list` *(relative → `/major/promotion/list`)* | — | `views/promotion/promotion_list` | `offers.list` | editor | — | no_scroll | admin.js:259-268 |
| `major/promotion/add` | — | `views/promotion/promotion_add_edit` | `offers.create` | editor | — | no_scroll | admin.js:269-278 |
| `major/promotion/edit/:id` | — | `views/promotion/promotion_add_edit` | `offers.list.edit` | editor | — | no_scroll | admin.js:279-288 |
| `major/promotion/tak/add` | — | `views/promotion/promotion_tak_add_edit` | `offers.create.tak` | editor | — | no_scroll | admin.js:289-298 |
| `major/promotion/tak/edit/:id` | — | `views/promotion/promotion_tak_add_edit` | `offers.list.tak.edit` | editor | — | no_scroll | admin.js:299-308 |
| `major/promotion/autopromotion` | — | `views/promotion/autopromotion` | `offers.autopromotion` | editor | — | no_scroll | admin.js:309-317 |
| `major/offersIgnore` | — | `views/offersIgnore/offersIgnore-add-edit.vue` | `offersIgnore` | editor | — | — | admin.js:319-326 |
| `/comments` | `comments` | `views/customer/Comments/Comments.vue` | `comment` | editor | دیدگاه ها | breadcrumb | admin.js:328-348 |
| `/sms/create` | `create_sms` | `views/sms/create.vue` | `sms.create` | editor | ارسال پیامک | breadcrumb | admin.js:350-370 |
| `/notify` | `notify` | `views/notify/user.vue` | `notify.me` | editor | لیست نوتیفیکیشن های ذریافتی | breadcrumb | admin.js:371-391 |
| `/notify/create` | `create_notify` | `views/notify/create.vue` | `notify.create` | editor | ارسال نوتیفیکیشن | breadcrumb | admin.js:392-412 |
| `/wallet` | `wallet` | `views/wallet/wallet.vue` | `wallet` | editor | لیست کیف پول های شارژ شده | breadcrumb | admin.js:413-433 |
| `/reagent/edit/:id` | `reagent-edit` | `views/apps/customer/EditReagent` | `persons.list.sellers.edit` | editor | — | parent `product`, no_scroll | admin.js:434-444 |
| `/bulkCustomer/edit/:id` | `bulkCustomer-edit` | `views/apps/customer/EditBulkCustomer` | `persons.list.client.omde.edit` | editor | — | parent `product`, no_scroll | admin.js:445-455 |
| `/bulkCustomer/view/:id` | `bulkCustomer-view` | `views/customer/omde/ViewOmdeCustomer` | `persons.list.client.omde.view` | editor | — | parent `product`, no_scroll | admin.js:456-466 |
| `/singleCustomer/edit/:id` | `singleCustomer-edit` | `views/apps/customer/EditSingleCustomer` | `persons.list.client.single.edit` | editor | — | parent `product`, no_scroll | admin.js:467-477 |
| `/singleCustomer/view/:id` | `singleCustomer-view` | `views/customer/single-customer/ViewSingleCustomer` | `persons.list.client.single.view` | editor | — | parent `product`, no_scroll | admin.js:478-488 |
| `/customer/omde-list` | `omde-list-customer` | `views/customer/omde/OmdeList.vue` | `persons.list.client.omde` | editor | مشتریان عمده | breadcrumb | admin.js:489-507 |
| `/customer/omde-non-verified-list` | `omde-non-verified-list` | `views/customer/omde/OmdeNonVerifiedList.vue` | `persons.list.client.omde` | editor | مشتریان در انتظار | breadcrumb | admin.js:509-527 |
| `/customer/banker-list` | `banker-list` | `views/customer/omde/BankerList.vue` | `persons.list.client.omde` | editor | لیست بنکداران | breadcrumb | admin.js:529-547 |
| `/customer/shopkeeper-list` | `shopkeeper-list` | `views/customer/omde/ShopkeeperList.vue` | `persons.list.client.omde` | editor | لیست مغازه داران حضوری | breadcrumb | admin.js:549-567 |
| `/customer/omde-archived-list` | `omde-archived-list` | `views/customer/omde/OmdeArchivedList.vue` | `persons.list.client.omde` | editor | مشتریان بایگانی | breadcrumb | admin.js:569-587 |
| `/customer/reagent-list` | `raegentList` | `views/customer/reagentList/ReagentList.vue` | `persons.list.sellers` | editor | کارشناسان فروش | breadcrumb | admin.js:589-607 |
| `/customer/add` | `add-customer` | `views/apps/customer/AddCustomer.vue` | `persons.create` | editor | افزودن مشتری | breadcrumb | admin.js:609-628 |
| `/customer/list` | `list-customer` | `views/customer/single-customer/SingleCustomer` | `persons.list.client.single` | editor | لیست مشتریان | breadcrumb | admin.js:630-645 |
| `/report/order_item` | `report_order_item` | `views/report/order_item` | `report.order.item` | editor | — | — | admin.js:647-659 |
| `/report/product` | `report_product` | `views/report/product` | `report.product` | editor | — | — | admin.js:661-669 |
| `/finical/list` | `finical_list` | `views/finical/list` | `document.list` | editor | — | — | admin.js:670-683 |
| `/user/list` | — | `views/user/ListUser.vue` | `user.list` | editor | لیست مدیران | breadcrumb, no_scroll | admin.js:685-704 |
| `/user/create` | — | `views/user/UserCreate.vue` | `user.create` | editor | افزودن کاربر | breadcrumb, no_scroll | admin.js:706-725 |
| `/user/edit/:id` | — | `views/user/UserEdit.vue` | `user.list.edit` | editor | ویرایش نوع کاربر | breadcrumb, no_scroll | admin.js:727-747 |
| `/permission` | — | `views/user/Permission.vue` | `user.permission` | editor | دسترسی ها | breadcrumb, no_scroll | admin.js:749-768 |
| `/city/create` | — | `views/city/createCity.vue` | `cities.create` | editor | افزودن شهر | breadcrumb, no_scroll | admin.js:770-789 |
| `/city/list` | — | `views/city/cityList.vue` | `cities.list` | editor | لیست شهر ها | breadcrumb, no_scroll | admin.js:791-811 |
| `/statics` | `statics` | `views/statics/index.vue` | `statics` | editor | آمار | breadcrumb | admin.js:813-832 |
| `/newsletters` | `newsletters` | `views/newsletters/newsletters.vue` | *(commented out `newsletters`)* | editor | خبرنامه | breadcrumb | admin.js:834-853 |
| `/branch/add` | `add-branch` | `views/branch/AddBranches.vue` | `branch.create` | editor | افزودن شعب | breadcrumb | admin.js:855-874 |
| `/branch/edit/:id` | `edit-branch` | `views/branch/EditBranches` | `branch.create` | editor | ویرایش شعب | — | admin.js:876-885 |
| `/branch/list` | `list-branch` | `views/branch/ListBranches.vue` | `branch.list` | editor | لیست شعب | breadcrumb | admin.js:886-906 |
| `/workshop/add` | `add-workshop` | `views/workshop/AddWorkshop.vue` | `workshop.create` | editor | افزودن کارگاه | breadcrumb | admin.js:908-927 |
| `/workshop/edit/:id` | `edit-workshop` | `views/workshop/EditWorkshop` | `workshop.create` | editor | ویرایش کارگاه | — | admin.js:929-938 |
| `/workshop/list` | `list-workshop` | `views/workshop/ListWorkshop.vue` | `workshop.list` | editor | لیست کارگاه ها | breadcrumb | admin.js:939-959 |
| `/invoice/add` | `add-invoice` | `views/invoice/AddInvoice.vue` | `invoice.create` | editor | افزودن فاکتور | breadcrumb | admin.js:961-980 |
| `/invoice/edit/:id` | `edit-invoice` | `views/invoice/EditInvoice.vue` | `invoice.create` | editor | ویرایش فاکتورها | — | admin.js:982-991 |
| `/invoice/list` | `list-invoice` | `views/invoice/ListInvoice.vue` | `invoice.list` | editor | لیست فاکتورها | breadcrumb | admin.js:992-1012 |
**73 routes** in `adminRoute` (incl. 4 redirect-only entries).
### 3.4 Admin full-page routes — `fullPageAdminRoute` (src/router/admin.js:1016-1033)
| path | name | component | meta | line |
|---|---|---|---|---|
| `/auth/login` | `page-login` | `views/auth/Login.vue` | rule `editor` | admin.js:1017-1024 |
| `/reg` | `reg` | `views/reg.vue` | rule `editor` | admin.js:1025-1032 |
### 3.5 Shared full-page error routes — `src/router.js:104-135`
| path | name | component | meta.rule |
|---|---|---|---|
| `/pages/error-404` | `page-error-404` | `views/pages/Error404.vue` | editor (router.js:104-111) |
| `/pages/error-500` | `page-error-500` | `views/pages/Error500.vue` | editor (router.js:112-119) |
| `/pages/not-authorized` | `page-not-authorized` | `views/pages/NotAuthorized.vue` | editor (router.js:120-127) |
| `/pages/maintenance` | `page-maintenance` | `views/pages/Maintenance.vue` | editor (router.js:128-135) |
| `*` | — | redirect `/pages/error-404` | router.js:140-143 |
### 3.6 Branch build routes — `src/router/branch.js`
| path | name | component | meta.role | meta.rule | line |
|---|---|---|---|---|---|
| `/` | `main` | redirect → `/dashboard` | — | — | branch.js:2-6 |
| `/dashboard` | `dashboard` | `views/branch/dashboard.vue` | `dashboard.omde` | admin | branch.js:7-15 |
| `/products` | `product-list` | `views/apps/product/ProductList.vue` | `product.list` | editor | branch.js:16-26 |
| `/order/list` | — | `views/order/List.vue` | `order.list` | editor | branch.js:32-41 |
| `/buy/order/list` | — | `views/branch/buyList.vue` | `order.list` | editor | branch.js:42-51 |
| `/order/create` | — | `views/branch/order/create.vue` | `order.create` | editor | branch.js:61-70 |
| `/singleCustomer/view/:id` | `singleCustomer-view` | `views/customer/single-customer/ViewSingleCustomer` | `persons.list.client.single.view` | editor | branch.js:82-92 |
| `/singleCustomer/edit/:id` | `singleCustomer-edit` | `views/apps/customer/EditSingleCustomer` | `persons.list.client.single.edit` | editor | branch.js:93-103 |
| `/customer/add` | `add-customer` | `views/branch/addCustomer.vue` | `persons.create` | editor | branch.js:104-112 |
| `/customer/list` | `list-customer` | `views/customer/single-customer/SingleCustomer` | `persons.list.client.single` | editor | branch.js:113-121 |
| `/auth/login` (full-page) | `page-login` | `views/branch/login.vue` | — | editor | branch.js:161-169 |
---
## 4. Menu / Navigation
Defined in **`src/layouts/components/vertical-nav-menu/navMenuItems.js`**; exports `isBranch() ? branchMenu : adminMenu` (navMenuItems.js:609-611). Consumed by `VerticalNavMenu.vue` (vertical layout) and `HorizontalNavMenu.vue` (branch/horizontal layout — `themeConfig.js:23` sets `mainLayoutType` to `horizontal` for branch).
**Gating**: `VerticalNavMenu.vue:322` — `if ((item.permissionId && this.$hasPermission(item.permissionId)) || !item.permissionId)`. Additional server-driven removal at `VerticalNavMenu.vue:187-220` (`GET /roles/{ut}` → `removeItem` list filters by `permissionId`).
### 4.1 Admin menu
| Group (header / i18n) | Item i18n | url | slug | icon | permissionId | line |
|---|---|---|---|---|---|---|
| — | **پیشخوان** | null | — | `HomeIcon` | `dashboard` | 3-10 |
| ↳ | آمار عمده فروشی | `/dashboard/ecommerce` | dashboard-ecommerce | — | `dashboard.omde` | 12-18 |
| ↳ | آمار تک فروشی | `/dashboard/analytics` | dashboard-ecommerce | — | `dashboard.tak` | 19-25 |
| `Apps` / کاربری (`PackageIcon`) | **اشخاص** | null | — | `UserIcon` | `persons` | 33-38 |
| ↳ | اضافه کردن شخص | `/customer/add` | add-customer | — | `persons.create` | 40-46 |
| ↳ | لیست کارشناسان فروش | `/customer/reagent-list` | raegentList | — | `persons.list.sellers` | 47-53 |
| ↳ | لیست مشتریان عمده | `/customer/omde-list` | omde-customer-list | — | `persons.list.client.omde` | 54-60 |
| ↳ | لیست مشتریان در انتظار | `/customer/omde-non-verified-list` | omde-non-verified-list | — | `persons.list.client.omde` | 61-67 |
| ↳ | لیست مشتریان بایگانی | `/customer/omde-archived-list` | omde-archived-list | — | `persons.list.client.omde` | 68-74 |
| ↳ | لیست مشتریان تک | `/customer/list` | app-user-list | — | `persons.list.client.single` | 75-81 |
| ↳ | لیست بنکداران | `/customer/banker-list` | banker-list | — | **none** (commented) | 82-88 |
| ↳ | لیست مغازه داران حضوری | `/customer/Shopkeeper-list` *(capital S — route is `/customer/shopkeeper-list`)* | shopkeeper-list | — | **none** (commented) | 89-95 |
| | **شعب** | null | — | `GridIcon` | `branch` | 98-103 |
| ↳ | افزودن شعب | `/branch/add` | add-branch | — | `branch.create` | 105-111 |
| ↳ | لیست شعب | `/branch/list` | branch-list | — | `branch.list` | 112-118 |
| | **کارگاه** | null | — | `GridIcon` | `workshop` | 122-127 |
| ↳ | افزودن کارگاه | `/workshop/add` | add-workshop | — | `workshop.create` | 129-135 |
| ↳ | لیست کارگاه ها | `/workshop/list` | workshop-list | — | `workshop.list` | 136-142 |
| | **محصولات** | null | — | `ShoppingBagIcon` | `product` | 146-151 |
| ↳ | افزودن محصول | `/apps/product/add` | add-product | — | `product.create` | 153-159 |
| ↳ | محصولات | `/apps/product/list` | product-list | — | `product.list` | 160-166 |
| ↳ | تغییر درصد محصولات | `/product/change/percent` | change-Percent | — | `product.percent` | 167-173 |
| | **سفارش ها** | null | — | `BoxIcon` | `order` | 176-181 |
| ↳ | ثبت سفارش تک | `/order/create` | order-omde-list | — | `order.create` | 183-189 |
| ↳ | لیست سفارش های تک | `/order/list` | order-list | — | `order.list` | 190-196 |
| ↳ | سفارش های ساختنی | `/order/make` | order-makeOrder | — | `order.makeOrder` | 197-203 |
| ↳ | لیست سفارش های عمده | `/order/omde-list` | order-omde-list | — | `order.omde` | 204-210 |
| ↳ | لیست تامین | `/order/tamin-list` | order-tamin-list | — | `order.tamin` | 211-217 |
| | **مدیران** | null | — | `UserIcon` | `user` | 220-225 |
| ↳ | دسترسی ها | `/permission` | user-permission | — | `user.permission` | 227-233 |
| ↳ | افزودن مدیر | `/user/create` | add-user | — | `user.create` | 234-240 |
| ↳ | لیست مدیران | `/user/list` | user-list | — | `user.list` | 241-247 |
| | **کیف پول** | null | — | `CreditCardIcon` | `wallet` | 250-255 |
| ↳ | لیست کیف پول های شارژ شده | `/wallet` | wallet | — | `wallet.list` | 257-263 |
| | **دسته بندی ها** | null | — | `ListIcon` | `collection` | 266-271 |
| ↳ | افزودن دسته بندی | `/category/add` | add-category | — | `collection.create` | 273-279 |
| ↳ | لیست دسته بندی ها | `/category/list` | list-category | — | `collection.list` | 280-286 |
| | **اسناد مالی** | null | — | `DollarSignIcon` | `document` | 289-294 |
| ↳ | لیست اسناد | `/finical/list` | finical_list | — | `document.list` | 296-302 |
| ↳ | وارد کردن سند مالی مشتری | `/add/findoc` | findoc | — | `document.create` | 303-309 |
| | **تخفیفات** | null | — | `GiftIcon` | `offers` | 312-317 |
| ↳ | تعریف تخفیف تک | `/major/promotion/tak/add` | major_promotion_add | — | `offers.create.tak` | 319-325 |
| ↳ | تخفیف اتوماتیک | `/major/promotion/autopromotion` | major_promotion_list | — | `offers.autopromotion` | 326-332 |
| ↳ | تعریف تخفیف عمده | `/major/promotion/add` | major_promotion_add | — | `offers.create` | 333-339 |
| ↳ | لیست تخفیفات | `/major/promotion/list` | major_promotion_list | — | `offers.list` | 340-346 |
| | **استثناعات تخفیف** | `/major/offersIgnore` | offersIgnore | `SlashIcon` | `offersIgnore` | 349-356 |
| | **فاکتور انتقال** | null | — | `BookIcon` | `invoice` | 358-363 |
| ↳ | افزودن فاکتور | `/invoice/add` | add-invoice | — | `invoice.create` | 365-371 |
| ↳ | لیست فاکتورها | `/invoice/list` | invoice-list | — | `invoice.list` | 372-378 |
| | **گزارشات** | null | — | `FileTextIcon` | `report` | 382-387 |
| ↳ | گزارش تجمیعی سفارش و اسناد | `/report/order_item` | report_ordedr_item | — | `report.order.item` | 389-395 |
| ↳ | گزارش محصولات | `/report/product` | report_product | — | `report.product` | 396-402 |
| | **شهر ها** | null | — | `PercentIcon` | `cities` | 405-411 |
| ↳ | افزودن شهر | `/city/create` | create_city | — | `cities.create` | 413-419 |
| ↳ | لیست شهر ها | `/city/list` | city_list | — | `cities.list` | 420-426 |
| | **دیدگاه ها** | `/comments` | — | `MessageSquareIcon` | `comment` | 429-436 |
| | **پیامک** | null | — | `MailIcon` | `sms` | 437-442 |
| ↳ | ارسال پیامک | `/sms/create` | create_sms | — | `sms.create` | 444-450 |
| | **نوتیفیکیشن** | null | — | `BellIcon` | `notify` | 453-458 |
| ↳ | ارسال نوتیفیکیشن | `/notify/create` | create_notify | — | `notify.create` | 460-466 |
| | **آمار** | `/statics` | — | `BarChartIcon` | `statics` | 469-475 |
| | **خبرنامه** | `/newsletters` | — | `FileTextIcon` | `newsletters.newsletters` | 476-482 |
| `UI` / مدیریت (`LayersIcon`) | **پیکر بندی** | null | — | `SettingsIcon` | `config` | 490-495 |
| ↳ | مدیریت ثابت ها | `/config/manage/const` | manage-const | — | `config.statics` | 497-503 |
| ↳ | گزارش | `/config/manage/report` | manage-report | — | `config.report` | 504-510 |
| ↳ | مدیریت کیف پول | `/config/manage/wallet` | manage-const | — | `config.wallet` | 511-517 |
### 4.2 Branch menu — `navMenuItems.js:524-607`
| i18n | url | icon | permissionId | line |
|---|---|---|---|---|
| پیشخوان | `/dashboard` | `HomeIcon` | `dashboard` | 525-532 |
| اشخاص (group) | null | `UserIcon` | `persons` | 533-538 |
| ↳ اضافه کردن مشتری | `/customer/add` | `UserPlusIcon` | `persons.create` | 540-547 |
| ↳ لیست مشتریان | `/customer/list` | `UsersIcon` | `persons.list.client.single` | 548-555 |
| محصولات | `/products` | `ShoppingBagIcon` | `product.list` | 558-565 |
| فاکتور خرید | `/buy/order/list` | `ShoppingBagIcon` | `product.list` | 566-573 |
| سفارش ها (group) | null | `BoxIcon` | `order` | 582-587 |
| ↳ ثبت سفارش | `/order/create` | `InboxIcon` | `order.create` | 589-596 |
| ↳ لیست سفارش | `/order/list` | `ListIcon` | `order.list` | 597-604 |
---
## 5. ACL / Permissions
### 5.1 Two independent systems
**A. `vue-acl` (`src/acl/acl.js`)** — mostly vestigial.
| Item | Value | Line |
|---|---|---|
| Install | `Vue.use(AclInstaller)` | acl.js:5 |
| `initialRole` | `'admin'`, overridden by `JSON.parse(localStorage.userInfo).userRole` | acl.js:7-10 |
| `notfound` | `/pages/not-authorized` | acl.js:14 |
| `acceptLocalRules` | `true` | acl.js:16 |
| globalRules | `admin: AclRule('admin')`; `editor: AclRule('editor').or('admin')` | acl.js:17-20 |
Every route carries `meta.rule` of `'editor'` (or `'admin'` on the two dashboards) — see §3.3. Since the seeded role is `admin` and `editor` ORs `admin`, this never blocks anything in practice.
**B. Real permission system (`$hasPermission` + `meta.role`)** — the operative one.
| Piece | Definition |
|---|---|
| Check fn | `Vue.prototype.$hasPermission = (permission_id) => { const roles = store.state.users.roles; if(roles){ return roles.includes(permission_id) } }` — src/main.js:142-148 |
| Store of roles | `store.state.users.roles` (`undefined` until loaded) — src/store/users/index.js:26 |
| Loaded on login | `store/auth/moduleAuth.js:17` → `dispatch('users/getRoles', data.roles.map(x=>x.rid))` from `POST admin-login` |
| Loaded on refresh | `src/router.js:170-172` → `GET users/show` → `dispatch('users/getRoles', data.permission)` |
| Parent expansion | `users/getRoles` runs `checkParent(roles)` which, for each dotted key, adds every ancestor prefix — e.g. `order.list.sts.remove` implies `order.list.sts`, `order.list`, `order` — src/store/users/index.js:3-21, 64-66 |
| Route gate | `to.meta.role` checked in `beforeEach` — src/router.js:193-195 |
| Menu gate | `item.permissionId` — VerticalNavMenu.vue:322 |
| Button gate | `v-if="$hasPermission('…')"` in templates |
| Clear | `deleteRoles` mutation — src/store/users/index.js:44-46, 67-69 |
### 5.2 Complete permission catalog — `src/views/user/permissionConst.js`
Tree used by the permission editor (`views/user/Permission.vue`) as `a-tree` data. **172 declared keys** (a few key strings repeat with different titles, noted below).
| key | title (fa) | line |
|---|---|---|
| `dashboard` | پیشخوان | 4 |
| `dashboard.omde` | آمار عمده فروشی | 8 |
| `dashboard.tak` | آمار تک فروشی | 12 |
| `persons` | اشخاص | 18 |
| `persons.create` | اضافه کردن شخص | 22 |
| `persons.create.seller` | افزودن کارشناس فروش | 26 |
| `persons.create.client` | افزودن مشتری عمده | 30 |
| `persons.create.single` | افزودن مشتری تک | 34 |
| `persons.list.sellers` | لیست کارشناسان فروش | 40 |
| `persons.list.sellers.edit` | ویرایش اطلاعات کارشناس فروش | 44 |
| `persons.list.sellers.view.orders` | مشاهده سفارشات کارشناس فروش | 48 |
| `persons.list.sellers.view.documents` | مشاهده اسناد مالی کارشناس فروش | 52 |
| `persons.list.sellers.unable.order` | توقف سفارش کارشناس فروش | 56 |
| `persons.list.sellers.able.order` | فعال سازی سفارش کارشناس فروش | 60 |
| `persons.list.sellers.able` | فعال سازی حساب کاربری کارشناس فروش | 64 |
| `persons.list.sellers.unable` | غیر فعال سازی حساب کاربری کارشناس فروش | 68 |
| `persons.list.client.omde` | لیست مشتریان عمده | 74 |
| `persons.list.client.omde.view` | مشاهده اطلاعات مشتریان عمده | 78 |
| `persons.list.client.omde.edit` | ویرایش اطلاعات مشتری عمده | 82 |
| `persons.list.client.omde.view.orders` | مشاهده سفارشات مشتری عمده | 86 |
| `persons.list.client.omde.view.documents` | مشاهده اسناد مالی مشتری عمده | 90 |
| `persons.list.client.omde.unable.order` | توقف سفارش مشتری عمده | 94 |
| `persons.list.client.omde.able.order` | فعال سازی سفارش مشتری عمده | 98 |
| `persons.list.client.omde.able` | فعال سازی مشتری عمده | 102 |
| `persons.list.client.omde.unable` | غیر فعال سازی مشتری عمده | 106 |
| `persons.list.client.single` | لیست مشتریان تک | 112 |
| `persons.list.client.single.view` | مشاهده اطلاعات مشتریان تک *(duplicate key at 124: «مشاهده اطلاعات مشتری تک»)* | 116, 124 |
| `persons.list.client.single.edit` | ویرایش اطلاعات مشتری تک | 120 |
| `branch` | شعب | 132 |
| `branch.create` | افزودن شعب | 137 |
| `branch.list` | لیست شعب | 141 |
| `workshop` | کارگاه | 147 |
| `workshop.create` | افزودن کارگاه | 152 |
| `workshop.list` | لیست کارگاه ها | 156 |
| `product` | محصولات | 162 |
| `product.create` | افزودن محصول | 167 |
| `product.list` | لیست محصولات | 171 |
| `product.list.view` | مشاهده لیست محصولات *(duplicate at 184: «مشاهده اطلاعات جامع محصول»)* | 176, 184 |
| `product.list.edit` | ویرایش اطلاعات محصول | 180 |
| `product.list.variants` | تنوع های محصول | 188 |
| `product.list.variants.view` | مشاهده تنوع های محصول | 193 |
| `product.list.variants.edit` | ویرایش تنوع محصول | 197 |
| `product.list.variants.remove` | حذف تنوع محصول | 201 |
| `product.list.variants.add` | افزودن تنوع محصول | 205 |
| `product.list.remove` | حذف محصول | 211 |
| `product.list.group.edit` | تغییر دسته ای | 215 |
| `product.percent` | تغییر درصد محصولات | 221 |
| `order` | سفارشات | 227 |
| `order.list` | لیست سفارشات | 232 |
| `order.list.view` | مشاهده لیست سفارشات *(duplicate at 260: «مشاهده سفارش»)* | 237, 260 |
| `order.list.sts` | وضعیت سفارش | 241 |
| `order.list.sts.view` | مشاهده وضعیت سفارش | 246 |
| `order.list.sts.remove` | حذف وضعیت | 250 |
| `order.list.sts.submit` | ثبت وضعیت | 254 |
| `order.list.view.edit` | ویرایش سفارش | 264 |
| `order.list.pdf` | دریافت pdf | 268 |
| `order.omde` | سفارشات عمده | 274 |
| `order.omde.state` | وضعیت سفارش | 279 |
| `order.omde.state.view` | مشاهده وضعیت سفارش | 284 |
| `order.omde.state.submit` | ثبت وضعیت سفارش | 288 |
| `order.omde.state.remove` | حذف وضعیت سفارش | 292 |
| `order.omde.view` | مشاهده سفارش | 298 |
| `order.omde.view.edit` | ویرایش سفارش | 302 |
| `order.omde.view.remove` | حذف آیتم از سفارش | 306 |
| `order.omde.remove` | حذف سفارش | 310 |
| `order.omde.get` | دریافت pdf سفارش | 314 |
| `order.makeOrder` | سفارشات ساختنی | 320 |
| `order.makeOrder.state` | وضعیت سفارش | 325 |
| `order.makeOrder.state.view` | مشاهده وضعیت سفارش | 330 |
| `order.makeOrder.state.submit` | ثبت وضعیت سفارش | 334 |
| `order.makeOrder.state.remove` | حذف وضعیت سفارش | 338 |
| `order.makeOrder.view` | مشاهده سفارش | 344 |
| `order.makeOrder.view.edit` | ویرایش سفارش | 348 |
| `order.makeOrder.view.remove` | حذف آیتم از سفارش | 352 |
| `order.makeOrder.remove` | حذف سفارش | 356 |
| `order.makeOrder.get` | دریافت pdf سفارش | 360 |
| `order.tamin` | لیست تامین | 366 |
| `order.tamin.view` | مشاهده لیست تامین *(duplicate at 394: «مشاهده سفارش»)* | 371, 394 |
| `order.tamin.state` | وضعیت سفارش | 375 |
| `order.tamin.state.view` | مشاهده وضعیت سفارش | 380 |
| `order.tamin.state.submit` | ثبت وضعیت سفارش | 384 |
| `order.tamin.state.remove` | حذف وضعیت سفارش | 388 |
| `order.tamin.view.edit` | ویرایش سفارش | 398 |
| `order.create` | ثبت سفارش تک | 404 |
| `user` | مدیران | 410 |
| `user.create` | افزودن مدیر | 415 |
| `user.list` | لیست مدیران | 419 |
| `user.list.view` | مشاهده لیست مدیران | 424 |
| `user.list.edit` | ویرایش مدیر | 428 |
| `user.list.remove` | حذف مدیر | 432 |
| `user.list.create` | افزودن مدیر | 436 |
| `user.permission` | دسترسی ها | 442 |
| `user.permission.edit` | ویرایش دسترسی | 447 |
| `user.permission.create` | افزودن دسترسی | 451 |
| `collection` | دسته بندی ها | 459 |
| `collection.create` | افزودن دسته بندی | 464 |
| `collection.list` | لیست دسته بندی ها | 468 |
| `collection.list.view` | مشاهده لیست دسته بندی ها | 473 |
| `collection.list.edit` | ویرایش دسته | 477 |
| `collection.list.remove` | حذف دسته | 481 |
| `document` | اسناد مالی | 489 |
| `document.list` | لیست اسناد مالی | 494 |
| `document.list.view` | مشاهده لیست اسناد مالی | 499 |
| `document.list.edit` | ویرایش سند مالی | 503 |
| `document.list.remove` | حذف سند مالی | 507 |
| `document.list.deny` | رد کردن سند مالی | 511 |
| `document.list.confirm` | تأیید کردن سند مالی | 515 |
| `document.list.get` | دریافت pdf سند مالی | 519 |
| `document.create` | وارد کردن سند مالی مشتری | 525 |
| `offersIgnore` | استثناعات تخفیف | 531 |
| `offers` | تخفیفات | 535 |
| `offers.list` | لیست تخفیفات | 540 |
| `offers.list.view` | مشاهده لیست تخفیفات | 545 |
| `offers.list.edit` | ویرایش تخفیف عمده | 549 |
| `offers.list.tak.edit` | ویرایش تخفیف تک | 553 |
| `offers.list.remove` | حذف تخفیف عمده | 557 |
| `offers.list.tak.remove` | حذف تخفیف تک | 561 |
| `offers.create` | تعریف تخفیف عمده | 567 |
| `offers.create.tak` | تعریف تخفیف تک | 571 |
| `offers.autopromotion` | تخفیف اتوماتیک | 575 |
| `invoice` | فاکتور انتقال | 581 |
| `invoice.create` | افزودن فاکتور | 586 |
| `invoice.list` | لیست فاکتورها | 590 |
| `report` | گزارشات | 596 |
| `report.order.item` | گزارش تجمیعی سفارشات و اسناد مالی | 601 |
| `report.product` | گزارش محصولات | 605 |
| `cities` | شهرها | 611 |
| `cities.create` | افزودن شهر | 616 |
| `cities.list` | لیست شهرها | 620 |
| `cities.list.view` | مشاهده لیست شهرها *(duplicate at 633: «مشاهده شهر»)* | 625, 633 |
| `cities.list.remove` | حذف شهر | 629 |
| `cities.list.view.edit` | ویرایش شهر | 637 |
| `comment` | دیدگاه ها | 645 |
| `comment.view.all` | مشاهده لیست دیدگاه ها | 649 |
| `comment.state` | تغییر وضعیت دیدگاه | 653 |
| `comment.view` | مشاهده دیدگاه | 657 |
| `comment.view.edit` | ویرایش دیدگاه | 661 |
| `comment.remove` | حذف دیدگاه | 665 |
| `sms` | پیامک | 671 |
| `sms.create` | ارسال پیامک | 676 |
| `sms.create.single` | ارسال برای یک شماره تلفن | 681 |
| `sms.create.all` | ارسال برای همه کاربران | 685 |
| `sms.create.reagent` | ارسال برای کارشناسان فروش | 689 |
| `sms.create.omde` | ارسال برای مشتریان عمده | 693 |
| `sms.create.multiple` | ارسال برای چند شماره تلفن | 697 |
| `sms.list` | لیست پیامک های ارسال شده | 703 |
| `wallet` | کیف پول | 710 |
| `wallet.list` | لیست کیف پول های شارژ شده | 715 |
| `wallet.charge` | شارژ کیف پول | 719 |
| `wallet.percent` | تغییر درصد شارژ کیف پول | 723 |
| `notify` | نوتیفیکیشن | 729 |
| `notify.create` | ارسال نوتیفیکیشن | 734 |
| `notify.create.multiple.reagent` | ارسال برای کارشناسان فروش | 739 |
| `notify.create.single.customer` | ارسال برای یک مشتری | 743 |
| `notify.create.multiple.omde` | ارسال برای مشتریان عمده | 747 |
| `notify.create.multiple.customer` | ارسال برای همه مشتریان | 751 |
| `notify.create.single.admin` | ارسال برای یک مدیر | 755 |
| `notify.create.multiple.admin` | ارسال برای همه مدیران | 759 |
| `notify.me` | لیست نوتیفیکیشن های دریافتی | 765 |
| `config` | پیکربندی | 771 |
| `config.statics` | مدیریت ثابت ها | 775 |
| `config.statics.brand` | برند ها | 780 |
| `config.statics.brand.create` | افزودن برند | 785 |
| `config.statics.brand.edit` | ویرایش برند | 789 |
| `config.statics.brand.remove` | حذف برند | 793 |
| `config.statics.tags` | برچسب ها | 799 |
| `config.statics.tags.create` | افزودن برچسب | 804 |
| `config.statics.tags.edit` | ویرایش برچسب | 808 |
| `config.statics.tags.remove` | حذف برچسب | 812 |
| `config.wallet` | مدیریت کیف پول | 820 |
| `config.report` | گزارش | 824 |
| `statics` | آمار | 830 |
| `newsletters.newsletters` | خبرنامه | 834 |
| `custom` | سفارشی | 838 |
| `custom.config.statics.slider` | اسلایدر موبایل | 842 |
| `custom.config.statics.tags.banner.ids` | برچسب بنر اول سایت | 846 |
**Referenced in code but NOT in `permissionConst.js`** (would never be grantable through the UI tree):
`persons.create.customer.tak` (views/apps/customer/AddCustomer.vue:142), `persons.list.sellers.view` (views/customer/reagentList/ReagentList.vue:32).
### 5.3 Button-level permission checks — every `$hasPermission(...)` call site
| Permission | Call sites (`file:line`) |
|---|---|
| `cities.list.remove` | views/city/cityList.vue:25 |
| `cities.list.view` | views/city/cityList.vue:18, 36 |
| `cities.list.view.edit` | views/user/ListUser.vue:108; views/city/cityList.vue:40, 47, 58, 66, 79, 90 |
| `collection.list.edit` | views/apps/category/List.vue:16 |
| `collection.list.remove` | views/apps/category/List.vue:24 |
| `comment.remove` | views/customer/Comments/Comments.vue:96 |
| `comment.state` | views/customer/Comments/Comments.vue:25 |
| `comment.view` | views/customer/Comments/Comments.vue:92, 104 |
| `comment.view.edit` | views/customer/Comments/Comments.vue:132, 155, 163 |
| `config.statics` | views/manage/Const.vue:3 |
| `config.statics.brand` | views/manage/Const.vue:8, 44 |
| `config.statics.brand.create` | views/manage/Const.vue:26, 36, 62, 72 |
| `config.statics.brand.edit` | views/manage/Const.vue:324, 342, 391, 411 |
| `config.statics.brand.remove` | views/manage/Const.vue:331 |
| `config.statics.tags` | views/manage/Const.vue:81 |
| `config.statics.tags.create` | views/manage/Const.vue:94, 104 |
| `config.statics.tags.edit` | views/manage/Const.vue:358, 375, 421 |
| `config.statics.tags.remove` | views/manage/Const.vue:365 |
| `config.wallet` | views/manage/wallet.vue:3 |
| `custom.config.statics.slider` | views/manage/Const.vue:122, 195 |
| `custom.config.statics.tags.banner.ids` | views/manage/Const.vue:110 |
| `document.list.confirm` | views/finical/list.vue:12 |
| `document.list.deny` | views/finical/list.vue:30 |
| `document.list.edit` | views/finical/list.vue:48 |
| `document.list.remove` | views/finical/list.vue:58 |
| `document.list.get` | views/finical/list.vue:76 |
| `notify` | layouts/components/navbar/TheNavbarVertical.vue:16 |
| `offers.list.edit` / `offers.list.tak.edit` | views/promotion/promotion_list.vue:53 |
| `offers.list.remove` / `offers.list.tak.remove` | views/promotion/promotion_list.vue:62 |
| `order.create` | views/invoice/EditInvoice.vue:3; views/invoice/AddInvoice.vue:3; views/order/create.vue:2 |
| `order.list.pdf` | views/order/List.vue:16 |
| `order.list.sts` | views/order/List.vue:41, 47; views/branch/buyList.vue:37, 43; views/workshop/buyList.vue:37, 43 |
| `order.list.sts.remove` | views/order/List.vue:60; views/branch/buyList.vue:56; views/workshop/buyList.vue:56 |
| `order.list.sts.submit` | views/order/List.vue:72; views/branch/buyList.vue:68; views/workshop/buyList.vue:68 |
| `order.list.view` | views/order/List.vue:13, 90 |
| `order.list.view.edit` | views/order/List.vue:135, 172, 182, 186, 205; views/invoice/ListInvoice.vue:125; views/branch/buyList.vue:98, 139; views/workshop/buyList.vue:98, 139 |
| `order.makeOrder.get` | views/order/MakeOrder.vue:21 |
| `order.makeOrder.remove` | views/order/MakeOrder.vue:18 |
| `order.makeOrder.state` | views/order/MakeOrder.vue:34, 42 |
| `order.makeOrder.state.remove` | views/order/MakeOrder.vue:54 |
| `order.makeOrder.state.submit` | views/order/MakeOrder.vue:65 |
| `order.makeOrder.view` | views/order/MakeOrder.vue:16, 82 |
| `order.makeOrder.view.edit` | views/order/MakeOrder.vue:108, 134, 139, 144, 149, 191 |
| `order.omde.get` | views/order/OmdeList.vue:21 |
| `order.omde.remove` | views/order/OmdeList.vue:18 |
| `order.omde.state` | views/order/OmdeList.vue:34, 42 |
| `order.omde.state.remove` | views/order/OmdeList.vue:54 |
| `order.omde.state.submit` | views/order/OmdeList.vue:65 |
| `order.omde.view` | views/order/OmdeList.vue:16, 82 |
| `order.omde.view.edit` | views/order/OmdeList.vue:107, 134, 139, 152, 159, 160 |
| `order.omde.view.remove` | views/order/OmdeList.vue:161 |
| `order.tamin.state` | views/order/TaminList.vue:23, 32 |
| `order.tamin.state.remove` | views/order/TaminList.vue:44 |
| `order.tamin.state.submit` | views/order/TaminList.vue:55 |
| `order.tamin.view` | views/order/TaminList.vue:12, 72 |
| `order.tamin.view.edit` | views/order/TaminList.vue:114, 129, 130 |
| `persons.create` | views/apps/customer/AddCustomer.vue:5 |
| `persons.create.client` | views/apps/customer/AddCustomer.vue:6 |
| `persons.create.seller` | views/apps/customer/AddCustomer.vue:137 |
| `persons.create.customer.tak` | views/apps/customer/AddCustomer.vue:142 *(commented out — tab always shown, line 143)* |
| `persons.list.client.omde.able` | OmdeList.vue:50,119,170; OmdeNonVerifiedList.vue:50,119,168; OmdeArchivedList.vue:50,119,168; ShopkeeperList.vue:50,119,170; BankerList.vue:50,121,172 |
| `persons.list.client.omde.able.order` | OmdeList.vue:108,158; OmdeNonVerifiedList.vue:108,156; OmdeArchivedList.vue:108,156; ShopkeeperList.vue:108,158; BankerList.vue:109,160 |
| `persons.list.client.omde.edit` | OmdeList.vue:22,139; OmdeNonVerifiedList.vue:22,137; OmdeArchivedList.vue:22,137; ShopkeeperList.vue:22,139; BankerList.vue:22,141 |
| `persons.list.client.omde.unable` | OmdeList.vue:165; OmdeNonVerifiedList.vue:163; OmdeArchivedList.vue:163; ShopkeeperList.vue:165; BankerList.vue:167 |
| `persons.list.client.omde.unable.order` | OmdeList.vue:99,153; OmdeNonVerifiedList.vue:99,151; OmdeArchivedList.vue:99,151; ShopkeeperList.vue:99,153; BankerList.vue:99,155 |
| `persons.list.client.omde.view` | OmdeList.vue:29,142; OmdeNonVerifiedList.vue:29,140; OmdeArchivedList.vue:29,140; ShopkeeperList.vue:29,142; BankerList.vue:29,144 |
| `persons.list.client.omde.view.documents` | OmdeList.vue:43,148; OmdeNonVerifiedList.vue:43,146; OmdeArchivedList.vue:43,146; ShopkeeperList.vue:43,148; BankerList.vue:43,150 |
| `persons.list.client.omde.view.orders` | OmdeList.vue:36,145; OmdeNonVerifiedList.vue:36,143; OmdeArchivedList.vue:36,143; ShopkeeperList.vue:36,145; BankerList.vue:36,147 |
| `persons.list.sellers.able` | views/customer/reagentList/ReagentList.vue:61 |
| `persons.list.sellers.able.order` | ReagentList.vue:49 |
| `persons.list.sellers.edit` | ReagentList.vue:20, 29 |
| `persons.list.sellers.unable` | ReagentList.vue:54 |
| `persons.list.sellers.unable.order` | ReagentList.vue:44 |
| `persons.list.sellers.view` | ReagentList.vue:32 |
| `persons.list.sellers.view.documents` | ReagentList.vue:40 |
| `persons.list.sellers.view.orders` | ReagentList.vue:36 |
| `product.list.edit` | views/apps/product/ProductList.vue:24; views/report/product.vue:14 |
| `product.list.group.edit` | views/apps/product/ProductList.vue:108 |
| `product.list.remove` | views/apps/product/ProductList.vue:54 |
| `product.list.variants` | ProductList.vue:44, 181; AddProduct.vue:274 |
| `product.list.variants.add` | ProductList.vue:190; AddProduct.vue:282 |
| `product.list.variants.edit` | ProductList.vue:213; AddProduct.vue:297 |
| `product.list.variants.remove` | ProductList.vue:221; AddProduct.vue:306 |
| `product.list.view` | ProductList.vue:34; views/report/product.vue:23 |
| `sms.create` | views/sms/create.vue:57 |
| `sms.create.all` | views/sms/create.vue:18 |
| `sms.create.multiple` | views/sms/create.vue:27 |
| `sms.create.omde` | views/sms/create.vue:24 |
| `sms.create.reagent` | views/sms/create.vue:21 |
| `sms.create.single` | views/sms/create.vue:15 |
| `sms.list` | views/sms/create.vue:61, 64 |
| `user.list.create` | views/user/ListUser.vue:4 |
| `user.list.edit` | views/user/ListUser.vue:10, 41 |
| `user.list.remove` | views/user/ListUser.vue:20 |
| `user.permission.create` | views/user/Permission.vue:16, 36, 115 |
| `user.permission.edit` | views/user/Permission.vue:34 |
| `wallet.charge` | views/customer/single-customer/SingleCustomer.vue:18, 26, 37 |
| `wallet.list` | views/wallet/wallet.vue:3 |
| `wallet.percent` | views/wallet/wallet.vue:29 |
### 5.4 Permission checks inside table models (column visibility)
| Model file | Permission | Effect |
|---|---|---|
| views/order/orderListModel.js:14-17 | `order.tamin.state` | adds `sts_str` + `branch.name` columns |
| views/order/OmdeListModel.js:20-32 | `order.omde.state` | adds `sts` column with the 7-value status filter |
| views/order/TaminListModel.js:14-16 | `order.tamin.state` | adds `sts` column |
| views/order/makeOrderModel.js:19-22 | `order.tamin.state` | adds `sts_str_fa` column |
| views/customer/Comments/CommentListModel.js:12-14 | *(role check)* | adds `changeState` column |
---
## 6. Views
### 6.1 Full file listing under `src/views` (business + template)
Legend: **[B]** = business (reachable from a live route or embedded in one), **[T]** = Vuexy template demo / dead.
#### Root
| File | Purpose |
|---|---|
| `views/Callback.vue` | [T] Auth0 callback stub (auth0 disabled at main.js:36-38). |
| `views/DashboardAnalytics.vue` | [B] Single-retail (تک) dashboard; route `/dashboard/analytics`, role `dashboard.tak`. |
| `views/DashboardECommerce.vue` | [B] Wholesale (عمده) dashboard; route `/dashboard/ecommerce`, role `dashboard.omde`. |
| `views/Inp.vue` | [T] input scratch page, unrouted. |
| `views/reg.vue` | [B] Public registration page; route `/reg`, full-page layout. |
#### `views/auth`
| File | Purpose |
|---|---|
| `views/auth/Login.vue` | [B] Admin login form (mobile + password). |
#### `views/user`
| File | Purpose |
|---|---|
| `views/user/ListUser.vue` | [B] Admin (مدیر) list + inline edit modal + delete. |
| `views/user/UserCreate.vue` | [B] Create/edit admin form. |
| `views/user/UserEdit.vue` | [B] 14-line wrapper that renders `UserCreate` in edit mode. |
| `views/user/Permission.vue` | [B] Role (`ut`) ⇄ permission-tree editor. |
| `views/user/myData.vue` | [B] 33-line "my profile" landing (default redirect target of `/`). |
| `views/user/permissionConst.js` | [B] The 172-key permission tree (see §5.2). |
#### `views/customer`
| File | Purpose |
|---|---|
| `views/customer/omde/OmdeList.vue` | [B] Wholesale customer list (614 lines) — the richest list view. |
| `views/customer/omde/OmdeNonVerifiedList.vue` | [B] Pending-verification wholesale customers (612 lines). |
| `views/customer/omde/OmdeArchivedList.vue` | [B] Archived wholesale customers (612 lines). |
| `views/customer/omde/BankerList.vue` | [B] Bankers (بنکدار) list (620 lines). |
| `views/customer/omde/ShopkeeperList.vue` | [B] In-person shopkeepers list (614 lines). |
| `views/customer/omde/ViewOmdeCustomer.vue` | [B] Wholesale customer 360° detail page (376 lines): orders/documents/sms/transactions tabs. |
| `views/customer/omde/OmdeAdd.vue` | [B] Wholesale add form (215 lines, embedded). |
| `views/customer/omde/{OmdeListModel,OmdeListNonVerifiedModel,OmdeArchiveListModel,BankerListModel,shopkeeperListModel}.js` | [B] XTbl column models for the five lists above. |
| `views/customer/omde/{omdeCustomer,omdeOrder,omdeSms,omdeTransaction}.js` | [B] XTbl models used inside `ViewOmdeCustomer` tabs. |
| `views/customer/single-customer/SingleCustomer.vue` | [B] Retail customer list (142 lines) + wallet-charge actions. |
| `views/customer/single-customer/ViewSingleCustomer.vue` | [B] Retail customer detail (425 lines). |
| `views/customer/single-customer/SingleCustomerList.js` | [B] XTbl model. |
| `views/customer/single-customer/{singleOrder,singleSms,singleTransaction}.js` | [B] XTbl models for detail tabs. |
| `views/customer/reagentList/ReagentList.vue` | [B] Sales-expert (کارشناس فروش) list. |
| `views/customer/reagentList/ReagentListModel.js` | [B] XTbl model. |
| `views/customer/Comments/Comments.vue` | [B] Product-comment moderation. |
| `views/customer/Comments/CommentListModel.js` | [B] XTbl model. |
| `views/customer/{List,CustomerEdit,CustomerEditTab*}.vue`, `views/customer/cell-renderer/*` | [T] ag-Grid-era leftovers, unrouted. |
#### `views/apps/customer`
| File | Purpose |
|---|---|
| `views/apps/customer/AddCustomer.vue` | [B] 3-tab person creation shell (wholesale / sales-expert / retail). |
| `views/apps/customer/bulkCustomer.vue` | [B] Wholesale customer form (385 lines) — embedded + edit mode. |
| `views/apps/customer/ReagentCustomer.vue` | [B] Sales-expert form (304 lines). |
| `views/apps/customer/singleCustomer.vue` | [B] Retail customer form (131 lines). |
| `views/apps/customer/EditBulkCustomer.vue` | [B] 19-line wrapper → `bulkCustomer` with `:id`. |
| `views/apps/customer/EditReagent.vue` | [B] 19-line wrapper → `ReagentCustomer` with `:id`. |
| `views/apps/customer/EditSingleCustomer.vue` | [B] 16-line wrapper → `singleCustomer` with `:id`. |
| `views/apps/customer/{Edit,CustomerView}.vue`, `customer-edit/*` | [T] unrouted template leftovers. |
#### `views/apps/product`
| File | Purpose |
|---|---|
| `views/apps/product/AddProduct.vue` | [B] Product create form + inline variant manager (1044 lines — largest view). |
| `views/apps/product/EditProduct.vue` | [B] 71-line wrapper → `AddProduct` with `:id`/`:variant_id`. |
| `views/apps/product/ProductList.vue` | [B] Product list + variants drawer + bulk category change (694 lines). |
| `views/apps/product/ChangePercent.vue` | [B] Bulk wage-percent change tool (254 lines). |
| `views/apps/product/Varaint.vue` | [B] Variant editor component (396 lines) — embedded. |
| `views/apps/product/EditVariant.vue` | [B] 15-line wrapper. |
| `views/apps/product/comp/ProductUploader.vue`, `uploadMixin.js` | [B] Product image upload widget + mixin. |
#### `views/apps/category`
| File | Purpose |
|---|---|
| `views/apps/category/Add.vue` | [B] Category create/edit form (422 lines). |
| `views/apps/category/Edit.vue` | [B] 12-line wrapper. |
| `views/apps/category/List.vue` | [B] Category list (313 lines). |
| `views/apps/category/CategoryTbl.js` | [B] XTbl model. |
#### `views/order`
| File | Purpose |
|---|---|
| `views/order/List.vue` | [B] Retail order list + status timeline modal + order-detail modal (322 lines). |
| `views/order/OmdeList.vue` | [B] Wholesale order list (324 lines) — same structure + item removal. |
| `views/order/TaminList.vue` | [B] Supply (تامین) list (241 lines). |
| `views/order/MakeOrder.vue` | [B] Made-to-order (ساختنی) list (432 lines). |
| `views/order/create.vue` | [B] Retail order creation with cart (451 lines). |
| `views/order/Add.vue` | [T] 170-line older add form, unrouted. |
| `views/order/{orderListModel,OmdeListModel,TaminListModel,makeOrderModel,product,workshopModel}.js` | [B] XTbl models. |
#### `views/finical`
| File | Purpose |
|---|---|
| `views/finical/addFinDoc.vue` | [B] Financial-document create/edit; 4 doc types (382 lines). |
| `views/finical/list.vue` | [B] Financial-document list with confirm/deny/pdf (166 lines). |
| `views/finical/list_model.js` | [B] XTbl model. |
#### `views/promotion` & `views/offersIgnore`
| File | Purpose |
|---|---|
| `views/promotion/promotion_list.vue` | [B] Promotion list (151 lines). |
| `views/promotion/promotion_add_edit.vue` | [B] Wholesale promotion create/edit (415 lines). |
| `views/promotion/promotion_tak_add_edit.vue` | [B] Retail promotion create/edit (388 lines). |
| `views/promotion/autopromotion.vue` | [B] Automatic-discount config (92 lines). |
| `views/offersIgnore/offersIgnore-add-edit.vue` | [B] Discount-exception editor (259 lines). |
#### `views/branch`
| File | Purpose |
|---|---|
| `views/branch/AddBranches.vue` | [B] Branch create (108 lines). |
| `views/branch/EditBranches.vue` | [B] Branch edit (112 lines). |
| `views/branch/ListBranches.vue` | [B] Branch list (88 lines). |
| `views/branch/ListBranch.js` | [B] XTbl model. |
| `views/branch/dashboard.vue` | [B] Branch-build dashboard (186 lines). |
| `views/branch/login.vue` | [B] Branch-build login (82 lines). |
| `views/branch/addCustomer.vue` | [B] 19-line branch customer wrapper. |
| `views/branch/buyList.vue` | [B] Branch purchase-invoice list (262 lines). |
| `views/branch/order/create.vue` | [B] Branch order creation. |
| `views/branch/orderListModel.js` | [B] XTbl model. |
#### `views/workshop`
| File | Purpose |
|---|---|
| `views/workshop/AddWorkshop.vue` | [B] Workshop create (117 lines). |
| `views/workshop/EditWorkshop.vue` | [B] Workshop edit (122 lines). |
| `views/workshop/ListWorkshop.vue` | [B] Workshop list (88 lines). |
| `views/workshop/ListWorkshop.js`, `orderListModel.js` | [B] XTbl models. |
| `views/workshop/{addCustomer,buyList,login}.vue` | [B/T] copies of the branch equivalents; only reachable via the workshop feature. |
#### `views/invoice`
| File | Purpose |
|---|---|
| `views/invoice/AddInvoice.vue` | [B] Transfer-invoice create with product picker (322 lines). |
| `views/invoice/EditInvoice.vue` | [B] Transfer-invoice edit (333 lines). |
| `views/invoice/ListInvoice.vue` | [B] Transfer-invoice list (217 lines). |
| `views/invoice/{AddInvoice,ListInvoice}.js` | [B] XTbl models. |
#### `views/city`
| File | Purpose |
|---|---|
| `views/city/createCity.vue` | [B] City create (166 lines). |
| `views/city/cityList.vue` | [B] City list with inline edit + percent-aux modal (303 lines). |
| `views/city/components/form.vue` | [B] Shared city form fields. |
| `views/city/CityListModel.js` | [B] XTbl model. |
#### `views/manage`, `views/statics`, `views/report`, `views/wallet`, `views/sms`, `views/notify`, `views/newsletters`
| File | Purpose |
|---|---|
| `views/manage/Const.vue` | [B] Constants manager: brands, tags, mobile slider, site banner (977 lines). |
| `views/manage/Const_new.vue` | [T] 256-line WIP rewrite, unrouted. |
| `views/manage/wallet.vue` | [B] Wallet config: birthday credit + per-order charge percent (142 lines). |
| `views/manage/report.vue` | [B] Metabase/dashboard-link embed (65 lines). |
| `views/statics/index.vue` | [B] Product-view analytics by path/customer (207 lines). |
| `views/report/order_item.vue` | [B] Consolidated order-item + document report (133 lines). |
| `views/report/product.vue` | [B] Product/variant report (111 lines). |
| `views/report/order_item_model.js` | [B] XTbl model. |
| `views/wallet/wallet.vue` | [B] Charged-wallet list + percent config (103 lines). |
| `views/wallet/walletTbl.js` | [B] XTbl model. |
| `views/sms/create.vue` | [B] SMS blast composer (265 lines). |
| `views/sms/list.vue` | [B] Sent-SMS list (115 lines) — not separately routed; used by `create.vue`. |
| `views/notify/create.vue` | [B] Push-notification composer (156 lines). |
| `views/notify/user.vue` | [B] Received-notification list (110 lines). |
| `views/notify/notifyTbl.js` | [B] XTbl model. |
| `views/newsletters/newsletters.vue` | [B] Newsletter subscriber list (59 lines). |
| `views/newsletters/newsletter.js` | [B] XTbl model. |
#### `views/apps` (template / partial)
| File | Purpose |
|---|---|
| `views/apps/finance/{BagList,DebitList,FeeList,OrderList}.vue` | [T] earlier finance lists, unrouted. |
| `views/apps/logs/{LogsList,Rules}.vue` | [T] unrouted. |
| `views/apps/promotions/{AddPromotion,Editpromotion,PromotionList,VariantPromotion}.vue` | [T] superseded by `views/promotion/*`, unrouted. |
| `views/apps/ticket/*` (5 files) | [T] unrouted. |
| `views/apps/user/*` (9 files) | [T] Vuexy user-management demo, unrouted. |
| `views/apps/{calendar,chat,email,todo,eCommerce}/*` | [T] Vuexy demo apps, unrouted. |
#### Pure template demos (unrouted, not enumerated individually)
`views/components/**` (~120 files), `views/charts-and-maps/**` (~40), `views/forms/**` (~50), `views/ui-elements/**`, `views/pages/{Error404,Error500,NotAuthorized,Maintenance}.vue` (routed — see §3.5).
**Business-view count: ~95 files** (`.vue` + XTbl `.js` models) under the directories listed above.
---
### 6.2 Significant views in detail
#### `views/order/List.vue` — Retail order list (322 lines)
| Aspect | Detail |
|---|---|
| Table model | `views/order/orderListModel.js` → `POST https://henzagold.com/oapi/admin/single-order` (orderListModel.js:4) |
| Title | `is_branch ? 'لیست سفارش' : 'لیست سفارش های تک فروشی'` (List.vue:6, 238-240) |
| Columns | `id` (شماره, filterNumber), `customer.fname` (نام مشتری, text), `customer.lname` (نام خانوادگی, text), `total_price` (جمع کل سفارش, text, slot `total_price` → `| priceUnit`), `items` (تعداد آیتم ها, slot `items_len` = `text.length`, noSort), `caj` (تاریخ ثبت, filterDate, noSort), `uaj` (تاریخ ویرایش, **hidden**, filterDate, noSort), `user` (ثبت کننده, slot, noSort), `opt` (عملیات, slot, noSort) — orderListModel.js:5-19 |
| Conditional columns | if role `order.tamin.state`: `sts_str` (وضعیت) + `branch.name` (نام شعبه) — orderListModel.js:14-17 |
| Row actions | مشاهده سفارش (`ListIcon`, gate `order.list.view`, opens detail modal) — List.vue:13-15; دانلود pdf (`FileIcon`, gate `order.list.pdf`) → `window.open('https://henzagold.com/oapi/admin/order/pdf/{id}','_blank')` — List.vue:16-18, 295-297 |
| Modal 1: status timeline | gate `order.list.sts`; `ATimeline` over `tempRow.activity`; color `green` if `item.proceed` else `gray` (List.vue:246-248). Per step: submit → `POST single/order-sts/edit` with the item (gate `order.list.sts.submit`, List.vue:72-78, 258-267); remove → same endpoint with `row.remove = true` (gate `order.list.sts.remove`, index > 0 only, List.vue:60-68, 268-278). Both close the modal, `$message.info('ویرایش موفق آمیز')` / `$message.error('خطا در ویرایش')`, then `this.$refs.tbl.fetch()`. Confirmation uses `APopconfirm` with title «آیا مطمئن به حذف وضعیت مورد نظر هستید ؟», ok `بله`, cancel `بی خیال`. `showSts` is a **no-op when `row.branch_id` is set** (List.vue:249-254). |
| Modal 2: order detail | gate `order.list.view`, width `80%`. `ADescriptions`: مشتری, تلفن همراه, تاریخ ثبت, وضعیت, نام دریافت کننده محصول, تلفن همراه دریافت کننده, تلفن ثابت دریافت کننده (List.vue:93-115); آدرس ارسال block: استان, شهرستان, کد پستی, آدرس (List.vue:116-129). Items card: thumbnail via `ZoomableImage` (falls back to `assets/images/no-image.png`), نام محصول (`ATag` blue), عنوان تنوع, وزن نهایی (`AInputNumber` when `editMode && order.list.view.edit`, else red `ATag`), تعداد (editable likewise), توضیحات (List.vue:140-197). Footer: `باشه` + `ویرایش سفارش` (gate `order.list.view.edit`). **`editRow()` has its `axios.put('order/'+id, …)` commented out** (List.vue:279-289) — it only toggles state and refetches. |
| Export | inherited from XTable toolbar (excel / json / print). |
#### `views/order/OmdeList.vue` / `TaminList.vue` / `MakeOrder.vue`
Same three-part shape (list + status timeline + detail modal), differing in endpoint, permission family and status vocabulary:
| View | Table URL | Permission family | Status endpoint |
|---|---|---|---|
| `OmdeList.vue` | `POST .../major/order/table` (OmdeListModel.js:7) | `order.omde.*` | `POST major/order-sts/edit` |
| `TaminList.vue` | `POST .../order-tamin` (TaminListModel.js:3) | `order.tamin.*` | `POST order-sts/edit` |
| `MakeOrder.vue` | `POST .../get-custom-orders` (makeOrderModel.js:4) | `order.makeOrder.*` | delete via `DELETE custom-order/{id}`, edit via `PUT custom-order/{id}` |
`OmdeList` adds row-level delete (`order.omde.remove`), pdf (`order.omde.get`), and per-item removal inside the detail modal (`order.omde.view.remove` → `DELETE major/order_item/`).
Wholesale order status filter (`OmdeListModel.js:21-32`):
`0: معلق`, `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی `, `5: تحویل مسئول فروش`, `6: تحویل مشتری `.
Customer-confirmation filter (OmdeListModel.js:33-38, TaminListModel.js:17-22): `1: تایید شده`, `2: در انتظار تایید`.
`MakeOrder` columns (makeOrderModel.js:5-24): `customer.id` (شماره), `customer.fname`, `customer.lname`, `color` (رنگ محصول), `size` (سایز محصول), `color_mina` (رنگ مینا), `color_stone` (رنگ سنگ), `sum` (تعداد), `time_creat_fa` (تاریخ ثبت, filterDate), `time_update_fa` (hidden), `customer.name` (ثبت کننده), `sts_str_fa` (وضعیت, only with `order.tamin.state`), `opt`. Admin-confirm sets `sts_str_fa = "تایید ادمین"` (MakeOrder.vue:397).
#### `views/order/create.vue` — Retail order creation (451 lines)
| Aspect | Detail |
|---|---|
| Page gate | `$hasPermission('order.create')` on the root element (create.vue:2) |
| Cart | vuex `cart` module: `cart/add` (create.vue:229), `cart/remove` (create.vue:32), `cart/updateItemQuantity` (create.vue:44), `cart/clear` (create.vue:276), `cart/init` (create.vue:307, 399) |
| Qty control | `AInputNumber` min `1`, max `variant.qty > 0 ? variant.qty : Infinity` (create.vue:44) |
| Bootstrap calls | `GET /order/type` → sale types (create.vue:395); `POST /single-customer` → `rows` as the customer dropdown (create.vue:397); `cart/init` (create.vue:399) |
| Fields (all ant `v-decorator`) | `customer_id` — **required**, «مقدار را وارد نمایید», searchable select (create.vue:77); `sts` — **required** (create.vue:84); `type` — **required** (create.vue:91); `shipping_address.reciver_fname`, `.reciver_lname`, `.province`, `.city`, `.zipcode`, `.reciver_phone`, `.reciver_fixed_phone` — all optional, same message (create.vue:98-116); `note` textarea `{minRows:3,maxRows:5}` optional (create.vue:120); `shipping_address.address` textarea optional (create.vue:123); hidden `app` initialValue `'app'` (create.vue:126); hidden `reagent_id` initialValue from data (create.vue:129) |
| `sts` enum (create.vue:158-165) | `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی`, `5: ارسال`, `6: تحویل مشتری` — **note: no `0: معلق` here, unlike the wholesale list filter** |
| Inline variant creation | `POST add/variant` with `{...variant, title: variant.grams + ' گرم'}` (create.vue:304) — the variant title is auto-derived from the weight |
| Product lookup | `GET product/show/{id}` (create.vue:360); `PUT product/update/{id}` (create.vue:341) |
#### `views/apps/product/AddProduct.vue` — Product create/edit (1044 lines)
Fields (`a-form-item` label → `v-decorator` key → rules):
| Label | Field | Rules | Line |
|---|---|---|---|
| کد محصول | `product_code` | **required**, «لطفا مقدار را وارد کنید» | 11-14 |
| نام محصول | `title` | **required**, same msg | 16-19 |
| نوع نمایش | `show_type` (radio) | **required**, initialValue `3` — `1: سایت`, `2: اپلیکشن موبایل`, `3: سایت و اپلیکشن موبایل` | 21-35 |
| دسته بندی | `cats` (multiple select) | **required**, «وارد کردن دسته بندی الزامی است» | 37-52 |
| برند | `brand_id` | **required** | 53-68 |
| سینی ها | `ciny_ids` | rules commented out (optional) | 69-99 |
| برچسب ها | `tags` | optional, «وارد کردن برچسب الزامی است » | 100-115 |
| شرح محصول (موبایل) | `body_html` | none (quill editor) | 116-121 |
| شرح محصول (سایت) | *(separate editor)* | none | 122-129 |
| نوع تحویل | `delivery_type` (radio) | **required**, initialValue `1` — `1: کوتاه مدت`, `2: بلند مدت` | 130-142 |
| متن پیام نمایش به کاربر در بازه بلند مدت | `message_long_delivery` | none; `disabled` unless `delivery_type == 2` | 143-149 |
| نمایش محصول | `is_active` (switch) | initialValue `true`, `valuePropName: 'checked'` | 150-152 |
| پیشنهاد روز | `offer_day` (switch) | initialValue `true` | 153-155 |
| تضمین قیمت | `has_warranty` (switch) | initialValue `true` | 156-158 |
| موجود | `is_exist` (switch) | initialValue `true` | 159-161 |
Variant sub-form (inside the same page, gated by `product.list.variants`, AddProduct.vue:274):
| Label | Field | Rules | Line |
|---|---|---|---|
| عنوان | `title` | **required** (empty msg) | 345-352 |
| تعداد | `qty` | **required** | 353-363 |
| وزن | `grams` | **required** | 364-371 |
| رنگ | `color` | **required**, «لطفا مقدار را وارد کنید»; options from `GET /product/color` (AddProduct.vue:857) | 372-380 |
| فعال | `is_active` (switch) | — | 381-383 |
| پیش سفارش | `is_pre_order` (switch) | — | 384-386 |
| اجرت ریالی | `rial_pay` | **required** | 391-399 |
| اجرت درصدی | `percentage_pay` | **required** | 400-409 |
| توضیح مختصر | `details` | none | 411-413 |
| توضیحات کامل | `description` | none | 414-416 |
| *(hidden)* | `product_id_id`, `id` | none | 417-418 |
Variant actions gated: add `product.list.variants.add` (282), edit `product.list.variants.edit` (297), remove `product.list.variants.remove` (306).
API calls: `POST product/create` (660); `PUT product/update/{id}` (698); `GET product/show/{id}` (718); `GET /product/color` (857); `POST category` (976); `GET brand?text_search=%` (986); `GET /const/__list__tags` (989); `GET https://henzagold.com/oapi/admin/ciny` (991).
#### `views/apps/product/ProductList.vue` (694 lines)
| Aspect | Detail |
|---|---|
| Model | `src/table/product.js:20` → `POST .../products`, title «لیست محصولات» |
| Columns | `id` (کد, filterNumber), `ProductImage` (تصویر محصول, slot `image`, noSort), `title` (نام محصول, text), `brand.title` (برند, text), `category.collection.title` (دسته بندی, text), `product_code` (کد محصول, text), `show_type` (نوع نمایش, filterSelect), `action` (عملیات, slot `op`, noSort) — table/product.js:21-32 |
| Row actions | ویرایش (`product.list.edit`, line 24) → `window.open('/admin/apps/product/edit/{id}','_blank')` (648); مشاهده (`product.list.view`, 34); تنوع ها (`product.list.variants`, 44) → `POST get/variants {id}` (631); حذف (`product.list.remove`, 54) → `POST delete/product/{id}` then `$refs.tblProducts.fetch()` (670-672) |
| Variant drawer actions | add → `window.open('/admin/apps/product/{id}/variant/add','_blank')` (546); toggles → `GET product/warranty/{id}` (556), `GET product/exist/{id}` (558); offer toggle → `GET /product/offer/{id}` (570); variant reload → `GET product/{product_id}/variant` (665) |
| Bulk edit | multi-select rows → card «تغییر دسته ای» (gate `product.list.group.edit`, line 108-114) with confirm «آیا مطمئن به تغییر {n} محصول هستید ؟» → `POST /change/all` (582) |
#### `views/apps/customer/*` — person forms
All three tabs share the ant `v-decorator` pattern; **every rule is `required: false`** — validation is effectively server-side only.
**`bulkCustomer.vue`** (wholesale, 385 lines) fields: `fname` (نام, 17), `lname` (نام خانوادگی, 24), `ncode` (کد ملی, 31), `password` (رمز عبور, 40), `mobile` (شماره تلفن همراه, 48) with pattern `/^(?:0|98|\+98|\+980|0098|098|00980)?(9\d{9})$/` msg «تلفن وارد شده نامعتبر است», `type` (نوع کاربری, 61), `store_name` (نام مغازه, 71), `phone` (تلفن, 78), `pasazh_name` (نام پاساژ, 85), `city_id` (شهر, 94, msg «شهر را انتخاب نمایید»), `mcode` (کد مشتری ته حساب, 106), a Jalali date field (116, msg «تاریخ دریافت را وارد نمایید»), image uploads عکس جواز کسب / عکس سردر مغازه / عکس پروفایل (119-143), `address` (آدرس, 147), `reagent_id` (کارشناس فروش, 164/182, msg «کارشناس فروش را انتخاب نمایید»), `percent` (درصد روی محصول, 197, msg «درصد روی محصول را وارد نمایید»), `gram_level` (سقف اعتبار, 204, msg «سقف اعتبار را وارد نمایید»).
Calls: `GET cities/get-percent-aux/{city_id}` on city change (265); `dispatch('customer/omdeSave', values)` (296); `GET query/reagent` (315); `POST cities/list` (321); `GET customer/{id}` in edit mode (330).
**`ReagentCustomer.vue`** (304 lines) — same field set minus wholesale-only fields; mobile pattern differs: `/^09(0[1-2]|1[0-9]|3[0-9]|2[0-1])-?[0-9]{3}-?[0-9]{4}$/` (line 52). Calls `GET cities/get-percent-aux/{v}` (221), `dispatch('customer/omdeSave')` (245), `POST cities/list` (265), `GET customer/{id}` (274).
**`singleCustomer.vue`** (retail, 131 lines) fields: `fname` (9), `lname` (16), `phone` (24), Jalali date (34), `city_id` (40). Calls: `PUT single/{id}` on edit (90), `POST single` on create (92), `POST cities/list` (110), `GET single/{id}` (119).
#### `views/finical/addFinDoc.vue` — Financial documents (382 lines)
Document type (`type`, initialValue `2`, addFinDoc.vue:18-32):
| value | label |
|---|---|
| `1` | آب شده (melted gold) |
| `2` | حواله (transfer) |
| `3` | چک (cheque) |
| `4` | پولی (cash) |
Fields shown conditionally on `form.getFieldValue('type')`:
| Label | Field | Rule | Applies to | Line |
|---|---|---|---|---|
| نوع سند | `type` | optional, initial `2` | all | 18 |
| مشتری | `customer_id` | **required** | all | 39/46 |
| توضیحات | `note` | optional | all | 53 |
| وزن (گرم) | `original_value` | **required** | آب شده | 70 |
| عیار | `ayar` | **required** | آب شده | 76 |
| نام آزمایشگاه | `lab` | optional | آب شده | 89 |
| انگ | `ang` | optional | آب شده | 95 |
| وزن (گرم) | `value` | **required** | حواله | 110 |
| حواله به | `to` | optional, initialValue `'حواله کوثر'`; options = `'حواله کوثر'` + `havale_list` | حواله | 121 |
| مبلغ چک | `price` | **required** | چک | 138 |
| وزن (گرم) | `value` | optional | چک | 144 |
| نام صاحب حساب | `account_name` | optional | چک | 151 |
| نام بانک | `bank_name` | optional | چک | 157 |
| میزان پول | `price` | **required** | پولی | 174 |
| مظنه | `price_per_gram` | **required** | پولی | 180 |
`views/finical/list.vue` row actions: تایید (`document.list.confirm`, line 12), رد (`document.list.deny`, 30), ویرایش (`document.list.edit`, 48), حذف (`document.list.remove`, 58), pdf (`document.list.get`, 76).
List columns (`views/finical/list_model.js`, `POST .../cash/table`): `id` (شماره سفارش, number), `type_str` (نوع سند, text), `price` (پول), `value` (وزن نهایی (گرم)), `to` (به, hidden), `customer.name` (نام و نام خانوادگی مشتری), `note` (توضیحات), `ayar` (عیار, number), `lab` (آزمایشگاه), `recive_date` (تاریخ دریافت, date), `account_name` (hidden), `bank_name` (hidden), `caj` (تاریخ ثبت, date), `uaj` (hidden), `confirmed` (وضعیت تایید سند, filterSelect `{0:'تایید نشده', 1:'تایید شده'}`, list_model.js:19-22), `price_per_gram` (مظنه, hidden), `original_value` (وزن طلای داده شده, hidden), `ang` (انگ, hidden), `opt`.
#### `views/user/UserCreate.vue` + `views/user/ListUser.vue` — Admin CRUD
Shared field rules (UserCreate.vue:7-60, mirrored in ListUser.vue:48-100 edit modal):
| Label | Field | Rules |
|---|---|---|
| نام | `fname` | required «لطفا مقدار را وارد کنید»; pattern `/^([\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C ])+$/` msg «فیلد نام باید شامل حروف فارسی باشد.» |
| نام خانوادگی | `lname` | same as `fname` |
| شماره موبایل | `mobile` | required; pattern `/(\+98\|0\|98\|0098)?([ ]\|-\|[()]){0,2}9[0-9]([ ]\|-\|[()]){0,2}(?:[0-9]([ ]\|-\|[()]){0,2}){8}/` msg «فرمت شماره موبایل نادرست است.»; `min: 11` msg «شماره موبایل باید 11 رقم باشد » |
| نوع کاربری | `ut` | required «وارد کردن مقدار الزامی است»; options from `GET const/__list__ut` (UserCreate.vue:169) |
| رمز عبور | `password` | required (multi-rule block, UserCreate.vue:43-50) |
| تأیید رمز عبور | `password_confirmation` | required + match check (UserCreate.vue:55-62) |
Calls: create `POST /users` (UserCreate.vue:108); update `PUT /users/{route.params.id}` (UserCreate.vue:104) and `PUT /users/{tempRow.id}` from the list modal (ListUser.vue:162); delete `DELETE users/{id}` (ListUser.vue:224) followed by `$refs.tbl.fetch()`.
List model `src/table/userTbl.js:2`: `GET/POST /users/list`; columns `id` (کد مدیر, noFilter), `fname` (نام, text), `lname` (نام خانوادگی, text), `mobile` (موبایل, text), `ut` (نوع کاربر, number, slot `ut`), `op` (عملیات, slot, noSort).
#### `views/user/Permission.vue` — Role/permission editor (175 lines)
| Field | Rule | Line |
|---|---|---|
| سطح کاربر (`ut`) | required, «لطفاسطح دسترسی را انتخاب کنید» | 9 |
| نام سطح دسترسی (`user`) | `required: newUT` (only when creating a new role) | 19 |
| نقش ها | `a-tree` over `permissionConst.js` | 23 |
Flow: load role list `GET /const/__list__ut` (113); load a role's grants `GET /roles/{id}` (136); create a new role `POST const {key:"__list__ut", val: values.user}` (77) then `POST roles/create {ut: newUtId, roles: rid}` (80); when editing, each removed key is deleted individually with a 500 ms stagger — `DELETE /roles/{ut}/{x}` inside `setTimeout(…, 500)` (86) — then `POST roles/create {ut, roles: rid}` (93).
#### `views/sms/create.vue` — SMS blast (265 lines)
`type` select (required, plus `{type:'number', message:'مقدار انتخاب شده نا معتبر است'}` — create.vue:13), options each gated:
| value | label | gate |
|---|---|---|
| `-1` | یک شماره تلفن | `sms.create.single` |
| `0` | همه کاربران | `sms.create.all` |
| `1` | کارشناسان فروش | `sms.create.reagent` |
| `2` | مشتریان عمده | `sms.create.omde` |
| `9` | چند شماره تلفن | `sms.create.multiple` |
Other fields: `mobile` (shown when `type === -1`) required + pattern `/^(\+98|0098|98|0)?9\d{9}$/g` msg «فرمت شماره موبایل نادرست است.» (35); `mobiles` (multi) required (43); `text` (متن پیامک) required (52).
Calls: sent-list `POST /sms/table` (168); test send `POST https://henzagold.com/oapi/api/sms/batch/test` (195); real send `POST https://henzagold.com/oapi/api/sms/batch` (216, 231). Note these two hit `/oapi/api/…`, **not** the `/oapi/admin/` base.
#### `views/notify/create.vue` — Push notification (156 lines)
| Field | Rule | Line |
|---|---|---|
| عنوان (`title`) | required «لطفا مقدار را وارد کنید» | 7 |
| نوع کاربر (`type_send`) | required | 13 |
| کاربر (`customer_id`) | required, shown when `type_send` ∈ {3, 5} | 31 |
| متن پیام (`note`) | required «مقدار را وارد نمایید» | 55 |
`type_send` enum (create.vue:83-90): `1: کارشناسان فروش`, `2: مشتریان عمده`, `3: یک مشتری`, `4: همه مشتریان`, `5: یک مدیر`, `6: همه مدیران`.
Calls: `POST /notify` (112); admin picker `POST /users/list` (128); customer picker `POST /notify/customer` (139).
Received list (`views/notify/user.vue` + `notifyTbl.js:3`): `POST /notify/table`, columns `id` (شماره), `title` (عنوان), `note` (متن), `opt`; `tbl.noFilters()` (notifyTbl.js:9). Mark-read `GET /notify/read/{id}`; detail `GET /notify/{id}`.
#### `views/manage/wallet.vue` — Wallet config (142 lines)
| Section | Field | Rule | Line |
|---|---|---|---|
| Birthday credit | `birthday_expire` (انقضا (روز)) | required | 13 |
| Birthday credit | `birthday_credit` (میزان (تومان)) | required | 18 |
| Per-order charge | `expire` (انقضا (روز)) | required | 34 |
| Per-order charge | `percent` (درصد) | required | 42 |
Calls: `GET /wallet/config` (77), `GET /wallet/birthday/config` (91), `POST /wallet/config` (109, 126). Page gated by `config.wallet` (wallet.vue:3).
#### `views/wallet/wallet.vue` — Charged-wallet list (103 lines)
Page gated by `wallet.list` (line 3); percent control gated by `wallet.percent` (line 29). Model `walletTbl.js:3` → `POST /wallet/table`; columns `id` (شماره), `customer.name` (نام مشتری, text), `order.id` (شماره سفارش, text), `value` (میزان شارژ, slot `value`), `expire_at` (تاریخ انقضا, slot `time`, noSort); `tbl.noFilters()` (walletTbl.js:10).
#### `views/statics/index.vue` — Product-view analytics (207 lines)
`mode` select (index.vue:6-17): `title` (عنوان مسیر), `customer/all` (تمام مشتریان), `customer/id` (یک مشتری — reveals a searchable customer select).
Calls: `POST log/{mode}` or `POST log/customer/{customer}` with `{start_at, end_at}` (index.vue:146-147); `POST` fallback (171); `GET log/ago/{customer}` (174); customer list `GET /log/list/customer` (188). Columns for the `customer/*` modes nest `customer.id` (شناسه), `customer.name` (نام), `customer.mobile` (موبایل) under «مشتری», then `time` (زمان) and `title` (نام محصول) (index.vue:105-136).
#### Customer list views (`OmdeList` / `OmdeNonVerifiedList` / `OmdeArchivedList` / `BankerList` / `ShopkeeperList`)
Five near-identical 612–620-line views. Shared column model (differing only in URL):
| key | title | filter | flags |
|---|---|---|---|
| `id` | کد | number | |
| `fname` | نام | text | hidden |
| `lname` | نام خانوادگی | text | hidden |
| `name` | نام و نام خانوادگی | text | |
| `mobile` | موبایل | text | hidden, noSort |
| `city_id` | شهر | select(`store.state.users.cityFilters`), `filterKey('city_id')` | hidden, slot `city` |
| `reagent.name` | کارشناس فروش | text | noSort |
| `gram_level` | سطح اعتبار | number | noSort |
| `percent` | درصد | number | noSort |
| `remain` | بدهی گرمی | number | noSort |
| `rial_remain` | بدهی ریالی | number | noSort |
| `caj` | تاریخ ثبت | date | noSort |
| `uaj` | بروزرسانی | date | hidden, noSort |
| `sts_str` | وضعیت ثبت سفارش | — | slot `sts`, noSort |
| `active` | وضعیت حساب کاربری | select | slot `active`, noSort |
| `phone` | تلفن | text | hidden, noSort |
| `address` | آدرس | text | hidden, noSort |
| `store_name` | نام مغازه | text | hidden, noSort |
| `zone` | منطقه | text | hidden, noSort |
| `pasazh_name` | نام پاساژ | text | hidden, noSort |
| `opt` | عملیات | — | slot `opt`, noSort |
(OmdeListModel.js:8-42 and the four siblings, identical line numbering.)
| View | Table URL |
|---|---|
| OmdeList | `POST .../customer` (OmdeListModel.js:7) |
| OmdeNonVerifiedList | `POST .../major/verify/table` (OmdeListNonVerifiedModel.js:7) |
| OmdeArchivedList | `POST .../major/archive/table` (OmdeArchiveListModel.js:7) |
| BankerList | `POST .../banker` (BankerListModel.js:7) |
| ShopkeeperList | `POST .../shopkeeper` (shopkeeperListModel.js:7) |
Row actions (all five, gates listed in §5.3): ویرایش, مشاهده, مشاهده سفارشات, مشاهده اسناد مالی, توقف سفارش / فعال سازی سفارش, فعال سازی / غیر فعال سازی حساب. Endpoints used: `POST customer/toggle-active`, `POST customer/toggle-sts`, `POST major/verify`, `GET major/archive/{id}`, `GET major/dashboard/{id}`.
`ReagentList.vue` uses `POST .../reagent` (ReagentListModel.js:7) with `grams_sell` (میزان فروش) added and the shop/zone columns removed (ReagentListModel.js:21).
`SingleCustomer.vue` (`SingleCustomerList.js:3` → `POST .../single-customer`): `id` (کد, number), `fname` (نام, text), `lname` (نام خانوادگی, text, noSort), `phone` (تلفن تماس, text, noSort), `email` (ایمیل, text, noSort), `branch.name` (نام شعبه, text, noSort), `op` (عملیات). Wallet-charge actions gated by `wallet.charge` (SingleCustomer.vue:18, 26, 37).
#### Detail pages (`ViewOmdeCustomer.vue`, `ViewSingleCustomer.vue`)
Tabbed 360° views built from four XTbl models each:
| Tab | Wholesale model → URL | Retail model → URL |
|---|---|---|
| Customers | `omdeCustomer.js:4` → `.../major/customer` | — |
| Orders | `omdeOrder.js:4` → `.../major/order` | `singleOrder.js:4` → `.../single/order` |
| SMS | `omdeSms.js:3` → `.../major/sms` | `singleSms.js:3` → `.../single/sms` |
| Transactions | `omdeTransaction.js:3` → `.../major/cash` | `singleTransaction.js:3` → `.../single/transaction` |
Transaction columns (`omdeTransaction.js:4-7`): `id` (شماره), `order.id` (شماره سفارش, text, `setDefaultValue('-')`), `value` (میزان شارژ, slot), `expire_at` (تاریخ انقضا, noSort, `setDefaultFilter('jdate')`, `setDefaultValue('-')`).
SMS columns (`omdeSms.js:4-8`): `id`, `massage` (متن پیامک, text), `mobile` (شماره تلفن, slot `value`), `created_at` (تاریخ انقضا, `setDefaultFilter('jdate')`, `setDefaultValue('-')`), `sts` (وضعیت, slot).
Other calls: `GET single/{id}`, `GET single/order/{id}`, `GET single/report/{id}`, `GET single/wallet/{id}`, `GET log/ago/{id}`.
#### `views/report/*`
`order_item.vue` model (`order_item_model.js:3` → `POST .../report/order_item`) columns: `id` (کد آیتم), `product_title` (نام محصول), `variant_title` (عنوان تنوع), `original_rash_weight` (وزن خام), `rash_weight` (وزن نهایی), `qty` (تعداد), `percent` (درصد), `rial_pay` (اجرت ریالی), `note` (توضیحات), `order_id` (شماره سفارش), `order.caj` (تاریخ ثبت شفارش), `order.cashe.id` (شماره سند), `order.cashe.type_str` (نوع سند), `order.cashe.value` (جمع کل سند), `order.customer.fname`, `order.customer.lname`, `order.customer.name`, `order.parent.name` (کارشناس فروش) — order_item_model.js:4-21.
`product.vue` model (`src/table/variant.js:3` → `POST /product/report`) columns: `id` (کد تنوع), `product_id_id` (کد محصول, hidden), `product.title` (نام محصول, `filterKey('product')`), `title` (نام تنوع), `qty` (تعداد), `grams` (وزن), `color_str` (رنگ, slot `color`), `rial_pay` (اجرت ریالی), `percentage_pay` (اجرت درصدی), `op` (عملیات). Edit action → `window.open('/admin/apps/product/edit/{product.id}/{row.id}','_blank')` (product.vue:101), gate `product.list.edit` (line 14).
#### Other list models
| View | Model file:line | URL | Columns |
|---|---|---|---|
| Branch list | `views/branch/ListBranch.js:3` | `POST .../branch/table` | `name` (نام, text), `address` (آدرس, text), `phone` (شماره تلفن, number), `user.name` (مدیر شعبه, number), `op` (slot `actions`) |
| Branch buy list | `views/branch/orderListModel.js:4` | `POST .../branch/product/table` | `id` (شماره, number), `count_item` (تعداد آیتم ها), `sum_weight` (جمع وزن), `caj` (تاریخ ثبت, date), `uaj` (hidden), `sts_str` (وضعیت, conditional), `note` (توضیحات), `opt` |
| Workshop list | `views/workshop/ListWorkshop.js:3` | `POST .../workshop/table` | `name`, `address`, `phone` (number), `op` (slot `actions`); `_noDownload = true` (line 10) |
| Workshop buy list | `views/workshop/orderListModel.js:4` | `POST .../workshop/product/table` | `id`, `count_item`, `sum_weight`, `caj`, `uaj` (hidden), `opt` |
| Invoice list | `views/invoice/ListInvoice.js:4` | `POST .../branch/product/table` | `id` (ردیف, number), `name` (نام مشتری, text), `sum_weight` (جمع کل سفارش, number, noSort), `count_item` (تعداد آیتم ها, number, noSort), `caj` (تاریخ ثبت, date, noSort), `allocator_user.name` (ثبت کننده, slot `user`, noSort), `note` (توضیحات, text, noSort), `op` (slot `actions`) |
| Invoice product picker | `views/invoice/AddInvoice.js:3` | `POST .../products` | `id`, `ProductImage` (slot `image`), `title`, `min_gram` (وزن, number), `brand.title`, `category.collection.title`, `product_code`, `max_gram` (حداکثر وزن, hidden), `op`; `_noDownload = true` (line 16) |
| Order product picker | `views/order/product.js:3` | `POST .../products` | `id`, `ProductImage`, `title`, `brand.title`, `category.collection.title`, `product_code`, `min_gram` (hidden), `max_gram` (hidden), `op`; `_noDownload = true` |
| Category list | `views/apps/category/CategoryTbl.js:3` | `POST .../category` | `id` (شماره, number), `image` (تصویر, slot, noSort), `type_str` (نوع نمایش), `title` (عنوان دسته), `parent.title` (پدر دسته), …, `opt` |
| Promotion list | `src/table/list_model.js:3` | `POST .../major/promotion/table` | `id` (شماره تخفیف, number), `title` (عنوان), `description` (توضیحات), `type_str` (نوع تخفیف), `start_atj` (تاریخ شروع), `end_atj` (تاریخ پایان), `type_customer` (نوع مشتری, slot), `opt` |
| Comments | `views/customer/Comments/CommentListModel.js:4` | `POST .../comment/table` | `product_id` (hidden, number), `product.title` (نام محصول, slot `productName`), `customer.id` (hidden), `customer` (نام نویسنده, slot), `reply` (پاسخ, slot, noSort), `sts` (وضعیت, slot, noSort), `changeState` (conditional), `opt` |
| City list | `views/city/CityListModel.js:3` | `POST .../cities/list` | (model constructed with no `.add()` calls — columns built in the view) |
| Brand list | `src/table/brand.js:3` | `POST .../brand` | `id` (کد, number), `title` (نام برند, text) |
| Newsletter | `views/newsletters/newsletter.js:3` | `POST .../fashion/table` | `name` (نام, text), `mobile` (شماره موبایل, number), `op` (slot `actions`); `_noDownload = true` |
| Wholesale customer (alt) | `src/table/major_customer.js:3` | `POST .../major/table` | `id`, `lname` (hidden), `name` ×2, `mobile` (hidden), `gram_level`, `percent`, `remain`, `rial_remain`, `grams_sell`, `caj`, `uaj` (hidden) |
---
## 7. Store
`src/store/store.js` — root with `state/getters/mutations/actions` + 11 namespaced modules (store.js:39-60). `strict: process.env.NODE_ENV !== 'production'` (store.js:59).
| Module key | File | Purpose |
|---|---|---|
| `cart` | `store/cart/cart.js` | Retail order cart |
| `invoiceCart` | `store/invoiceCart/invoiceCart.js` | Transfer-invoice cart (near-identical copy of `cart`) |
| `auth` | `store/auth/moduleAuth.js` | **Live** auth (login, token/user persistence) |
| `category` | `store/category/index.js` | Category CRUD |
| `variant` | `store/variant/index.js` | Product variants |
| `product` | `store/product/index.js` | Product CRUD |
| `promotion` | `store/promotion/index.js` | Variant promotions |
| `order` | `store/order/index.js` | Order CRUD |
| `customer` | `store/customer/index.js` | Customer CRUD |
| `eCommerce` | `store/eCommerce/moduleECommerce.js` | Vuexy template demo |
| `variantpromotion` | `store/variantpromotion/index.js` | Per-variant promotion rows |
| `users` | `store/users/index.js` | Roles/permissions + city filter cache |
**Registered but commented out** (store.js:21-24): `todo`, `calendar`, `chat`, `email`. `manage/const.js` is imported as `moduleConst` but **not registered** (store.js:29, 48 — line 48 is commented).
Directories present and unused: `store/data-list/`, `store/user-management/`, `store/manage/index.js` (empty file).
### Root state — `src/store/state.js:73-91`
`AppActiveUser` (merged `localStorage.userInfo` over defaults, state.js:18-46), `bodyOverlay`, `isVerticalNavMenuActive`, `is_touch_device`, `mainLayoutType` (from themeConfig), `navbarSearchAndPinList`, `reduceButton`, `verticalNavMenuWidth`, `verticalNavMenuItemsMin`, `scrollY`, `starredPages`, `theme`, `themePrimaryColor`, `windowWidth`, `orderCount` (state.js:90).
Root mutations of note: `setOrderCount` (mutations.js:20-22), `UPDATE_USER_INFO` (mutations.js:100-121, also writes `localStorage.userInfo`), layout/theme toggles (mutations.js:23-92).
### `auth` — `src/store/auth/moduleAuth.js`
| Item | Detail |
|---|---|
| `state.user` | function reading `localStorage[isBranch()?'branch_user':'user']` (moduleAuth.js:5-7) — **a function, not a value**; `router.js:164` truth-tests it, so it is always truthy |
| `state.token` | function reading `localStorage[isBranch()?'branch_token':'token']` (moduleAuth.js:8-10) — same caveat |
| `state.userPermission` | `[]` (moduleAuth.js:11) — unused |
| action `login(payload)` | `POST admin-login` → `dispatch('users/getRoles', data.roles.map(x=>x.rid), {root:true})`, then `delete data.roles; delete data.ut`, `commit('INJECT', data)`; rethrows on error (moduleAuth.js:14-24) |
| mutation `INJECT` | writes `localStorage[token|branch_token] = payload.api_token`, `localStorage.login = true`, `localStorage[user|branch_user] = JSON.stringify(payload)` (moduleAuth.js:28-32) |
Dead siblings: `moduleAuthActions.js` (firebase + jwt flows, 362 lines), `moduleAuthState.js` (firebase `isUserLoggedIn`), `moduleAuthMutations.js` (`SET_BEARER`), `moduleAuthGetters.js` (empty).
### `users` — `src/store/users/index.js`
State: `cityFilters: {}`, `userInfo: undefined`, `roles: undefined` (index.js:23-27).
| Action | Endpoint / behavior |
|---|---|
| `getCities` | `POST cities/list` → `commit('setCities', res.data.rows)` which maps `{[x.id]: x.title}` (index.js:51-57, 30-34) |
| `setUserInfo` / `deleteUserInfo` | local only (index.js:58-63) |
| `getRoles(roles)` | `commit('getRoles', checkParent(roles))` — expands every dotted key to all ancestor prefixes (index.js:64-66, 3-21) |
| `deleteRoles` | clears `state.roles` (index.js:67-69) |
### CRUD modules — endpoints per action
| Module | Action | HTTP | Endpoint | Line |
|---|---|---|---|---|
| `order` | `remove(id)` | DELETE | `order/{id}` then `dispatch('order/fetch')` | order/index.js:42-43 |
| `order` | `save(payload)` | PUT / POST | `order/{id}` if `payload.id` else `order/` | order/index.js:54, 57 |
| `order` | `fetch` | GET | `order/` params `{page: state.page, text_search: state.searchQuery}` | order/index.js:69 |
| `product` | `init` | GET | `config/statics` | product/index.js:46 |
| `product` | `remove(id)` | POST | `delete/product/{id}` then `fetch` | product/index.js:58-59 |
| `product` | `save` | POST | `product` | product/index.js:78 |
| `product` | *(2nd save)* | PUT / POST | `product/{id}` else `product` | product/index.js:91, 94 |
| `product` | `fetch` | GET | `product/` params `{page, text_search}` | product/index.js:106 |
| `product` | *(variant)* | PUT | `product/variant/{id}` | product/index.js:115 |
| `customer` | `remove(id)` | DELETE | `user/profile/{id}` then `fetch` | customer/index.js:44-45 |
| `customer` | `save` | PUT / POST | `user/profile/{id}` else `user/profile/` | customer/index.js:56, 59 |
| `customer` | `omdeSave` | POST | `customer/create` | customer/index.js:71 |
| `customer` | `fetch` | GET | `user/profile` params `{page, text_search}` | customer/index.js:82 |
| `customer` | `getEditData({id})` | GET | `user/profile/{id}` | customer/index.js:94 |
| `category` | `remove(id)` | DELETE | `category/{id}` then `fetch` | category/index.js:39-40 |
| `category` | `save` | PUT / POST | `category/{id}` else `category/` | category/index.js:52, 55 |
| `category` | `fetch` | GET | `category/` params `{page, text_search}` | category/index.js:67 |
| `variant` | `init` | GET | `config/statics` | variant/index.js:38 |
| `variant` | `save` | POST | `add/variant` (both create and "update" paths) | variant/index.js:51, 54 |
| `variant` | `remove(row)` | DELETE | `/product/variant/{row.id}` | variant/index.js:66 |
| `promotion` | `init` | GET | `config/statics` | promotion/index.js:46 |
| `promotion` | `remove(id)` | DELETE | `product/variant/promotion/{id}` then `fetch` | promotion/index.js:58-60 |
| `promotion` | `save` | PUT / POST | `product/variant/promotion/{id}` else `product/variant/promotion` | promotion/index.js:70, 73 |
| `promotion` | `fetch` | GET | `product/variant/promotion` params `{page, text_search}` | promotion/index.js:85 |
| `variantpromotion` | `remove(id)` | DELETE | `product/variant/promotion/variant-promotion/{id}` | variantpromotion/index.js:58 |
| `variantpromotion` | `save` | PUT / POST | `product/variant/promotion/variant-promotion/{id}/` else `product/variant/promotion/variant-promotion` | variantpromotion/index.js:71, 75 |
| `variantpromotion` | `fetch(id)` | GET | `/product/variant/promotion/{id}/variant-promotion` | variantpromotion/index.js:88 |
| `cart` / `invoiceCart` | `init` | POST | `product/variant` body `{products_id: ids}` | cart/cart.js:73; invoiceCart.js:68 |
Shared list-module state shape (`order`, `customer`, `category`, `product`, `promotion`, `variantpromotion`): `{model, rows, count, page, loading, searchQuery}` with mutations `ADD_ITEM`, `LOADING`, `SET_PAGE`/`PAGE_SET`, `SEARCH_QUERY_SET`, `FETCH`, `INJECT_LIST`, `EDIT_ROW` (e.g. order/index.js:3-36).
### `cart` / `invoiceCart` business rules
| Rule | Evidence |
|---|---|
| Identity key is the pair `(pid, vid)` | cart/cart.js:11 |
| An item is only addable if `variant && !(!variant.is_pre_order && variant.qty === 0)` — i.e. out-of-stock is allowed only when the variant is pre-order | cart/cart.js:15; invoiceCart.js:15 |
| Re-hydration (`init`) re-applies the same stock rule against `variants_all[0]` and drops items that fail | cart/cart.js:79; invoiceCart.js:74 |
| Each rebuilt item carries `{vid, pid, product, qty, promotion_id: product.promotion?.id ?? null}` | cart/cart.js:80-86 |
| `INIT` is committed after every mutation (persistence hook) | cart/cart.js:52-68 |
---
## 8. Shared utils / filters / mixins
### `src/utils/index.js`
| Export | Behavior | Line |
|---|---|---|
| `isBranch()` | `process.env.NODE_ENV == 'branch'` (also `console.log`s the env) | utils/index.js:1-4 |
This single flag drives: token/user localStorage key (axios.js:6, moduleAuth.js:6/9, router.js:173/174/188), route table (router.js:27, 39), menu (navMenuItems.js:609), layout type (themeConfig.js:23), page titles (order/List.vue:6).
### `src/filters/filters.js` — Vue global filters
| Filter | Signature | Behavior | Line |
|---|---|---|---|
| `capitalize` | `(value)` | Title-cases each space-separated word | 4-14 |
| `title` | `(value, replacer='_')` | Splits on `replacer`, capitalizes, re-joins with space | 16-27 |
| `truncate` | `(value, limit)` | `value.substring(0, limit)` | 29-31 |
| `tailing` | `(value, tail)` | `value + tail` | 33-35 |
| `time` | `(value, is24HrFormat=false)` | `H:MM` (24h) or `h:MM بعد از ظهر / قبل از ظهر` (12h) | 41-53 |
| `date` | `(value, fullDate=false)` | Slices a JS `Date` string: `DD MMM` or `DD MMM YYYY` | 55-63 |
| `month` | `(val, showYear=true)` | Regex-extracts month (+ year) from a Date string | 65-75 |
| `csv` | `(value)` | `value.join(', ')` | 77-79 |
| `filter_tags` | `(value)` | Strips HTML tags via `/<\/?[^>]+(>|$)/g` | 81-83 |
| `k_formatter` | `(num)` | `num > 999 ? (num/1000).toFixed(1)+'هزار' : num` | 85-87 |
| **`price`** | `(val)` | Thousand separators: `val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")`; returns `val` unchanged when falsy (so `0` passes through) | 92-97 |
| **`priceUnit`** | `(val)` | Same as `price` + `' تومان '` suffix | 99-104 |
| **`jdateTime`** | `(val)` | `moment(val).format('jYYYY/jMM/jDD - hh:mm:ss')` — note **12-hour `hh` with no am/pm marker** | 107-112 |
| **`jdate`** | `(val)` | `moment(val).format('jYYYY/jMM/jDD')` | 114-119 |
| `dynamicFilter` | `(value, name, ...args)` | `Vue.filter(name)(value, ...args)` — used by XTbl `setDefaultFilter` | 120-122 |
| `none` | `val => val` | Identity — the default for `setDefaultFilter` | 123 |
**Notable absences**: there is **no gram/weight formatter**, **no Persian↔English digit-conversion utility**, and **no price-input mask** anywhere in the codebase. Weights are rendered raw (`{{item.grams}}`, order/List.vue:173) and entered through plain `a-input-number`. Prices are formatted only on read via `price`/`priceUnit`.
### `src/mixins/citiesModel.js`
| Method | Behavior | Line |
|---|---|---|
| `normalizeCityId(data)` | Clones `data`; if `city_id` is null and `city` is set, resolves it — from `city.id` when `city` is an object, or by title match against `this.cities` when it's a string; finally coerces `city_id` to `Number` | mixins/citiesModel.js:3-18 |
| `recursiveFunction(obj, level)` | Builds an ant `a-cascader`/`a-tree-select` node tree from the flat `this.cities` array using `level`/`parent`; `disabled: level === 0` (provinces not selectable); emits `{value:Number(id), label:title, key:Number(id), disabled, children?}` | mixins/citiesModel.js:19-46 |
`src/views/apps/product/uploadMixin.js` — product image upload helpers.
`src/components/input/mixins.js` — shared form-input mixin (see §9).
### Constants / enums (exact values)
| Domain | Values | Source |
|---|---|---|
| Wholesale order status (filter) | `0: معلق`, `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی `, `5: تحویل مسئول فروش`, `6: تحویل مشتری ` | views/order/OmdeListModel.js:23-29 |
| Retail order status (create form) | `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی`, `5: ارسال`, `6: تحویل مشتری` | views/order/create.vue:158-165 |
| Customer confirmation | `1: تایید شده`, `2: در انتظار تایید` | views/order/OmdeListModel.js:35-36; TaminListModel.js:19-20 |
| Financial document type | `1: آب شده`, `2: حواله`, `3: چک`, `4: پولی` | views/finical/addFinDoc.vue:19-31 |
| Document confirmation | `0: تایید نشده`, `1: تایید شده` | views/finical/list_model.js:20-21 |
| Product display type | `1: سایت`, `2: اپلیکشن موبایل`, `3: سایت و اپلیکشن موبایل` (default `3`) | views/apps/product/AddProduct.vue:23-33 |
| Delivery type | `1: کوتاه مدت`, `2: بلند مدت` (default `1`) | views/apps/product/AddProduct.vue:132-141 |
| SMS audience | `-1: یک شماره تلفن`, `0: همه کاربران`, `1: کارشناسان فروش`, `2: مشتریان عمده`, `9: چند شماره تلفن` | views/sms/create.vue:15-29 |
| Notification audience | `1: کارشناسان فروش`, `2: مشتریان عمده`, `3: یک مشتری`, `4: همه مشتریان`, `5: یک مدیر`, `6: همه مدیران` | views/notify/create.vue:83-90 |
| Statics mode | `title` (عنوان مسیر), `customer/all` (تمام مشتریان), `customer/id` (یک مشتری) | views/statics/index.vue:7-16 |
| Default transfer target | `'حواله کوثر'` + server `havale_list` | views/finical/addFinDoc.vue:121-127 |
| Const-manager keys | `{id:'brand', title:'برند ها', icon:'icon-user', iconPack:'feather'}` (`ut`/`status` entries commented out) | store/manage/const.js:6-25 |
| Theme colors | `primary:#1c2d56`, `secondary:#836447`, `third:#f5cb55`, `success:#28C76F`, `danger:#EA5455`, `warning:#FF9F43`, `dark:#1E1E1E` | themeConfig.js:5-14 |
---
## 9. Components
### 9.1 Table system — `src/components/x-table/`
The workhorse. Four near-duplicate copies exist: `x-table/` (**canonical**), `x-table_select/`, `custom-xtable-select/`, `backup-table/`. Only `x-table` is imported by business views.
**`model/XTbl.js`** — fluent table builder:
| Member | Purpose | Line |
|---|---|---|
| `constructor(url, title)` | sets the POST endpoint and the report title | XTbl.js:10-13 |
| `add(col)` | appends an `Xtc` | XTbl.js:15-20 |
| `addDefaultFilter(filter)` | pre-applied filter | XTbl.js:22-28 |
| `addOpt(...)` | appends an «عملیات» column with `noSort()` | XTbl.js:31-38 |
| `hidePagination()` / `hideSearch()` / `addExtraBtn()` | all set `_hidePageination = true` (the latter two are copy-paste bugs) | XTbl.js:41-59 |
| `getSortFields()` | cols where `__sorter` is truthy | XTbl.js:47-49 |
| `noSearch()` / `noSettings()` / `noFilters()` | set `_noSearch` / `_noSettings` / `_noFilters` | XTbl.js:61-74 |
**`model/Xtc.js`** — column builder. Defaults: `dataIndex = key`, `show = true`, `__sorter = true` (Xtc.js:4-12).
| Method | Effect | Line |
|---|---|---|
| `filterKey(key)` | overrides the server-side filter field (`__fkey`/`master`) | Xtc.js:33-37 |
| `renderSlot(name)` | sets `scopedSlots.customRender` | Xtc.js:39-48 |
| `filterText()` | `filterType = 'default'` | Xtc.js:59-63 |
| `filterSwitch()` | `filterType = 'switch'` | Xtc.js:65-70 |
| `filterQuery(action)` | `filterType = 'query'` + `filterAction` | Xtc.js:73-78 |
| `filterDate()` | `filterType = 'date'` | Xtc.js:80-84 |
| `filterSelect(items)` | `filterType = 'select'` + `filterItems` | Xtc.js:86-91 |
| `filterNumber()` | `filterType = 'number'` | Xtc.js:93-97 |
| `noSort()` | `__sorter = false` | Xtc.js:99-102 |
| `hidden()` | `show = false` (still toggleable in the column selector) | Xtc.js:104-107 |
| `setDefaultValue(v)` / `setDefaultFilter(v)` | delegate to `sedmedghAddon/XtcModel` mixins — `setDefaultFilter` names a Vue filter applied via `dynamicFilter` | Xtc.js:109-115 |
**`XTable.vue`** — the rendering component (ant `a-table` based):
| Aspect | Detail | Line |
|---|---|---|
| Query shape | `{search, filters: [], limit: 10, page: 1, order}` | XTable.vue:316-320 |
| Default page size | `10`; selectable `['5','10','20']` | XTable.vue:255-256, 319 |
| Sort encoding | `+field` (asc) / `-field` (desc) from the `sort` + `sortDirection` controls | XTable.vue:340-343 |
| Header-click sort | `this.query.order = ['{-|+}{columnKey}']` — **inverted vs. the dropdown**: `ascend` maps to `-` | XTable.vue:445-451 |
| Request body | `{...query, select: visibleCols.map(c=>c.key), titles: {key: title}, report_title: model.title}` (+ `op` when exporting) | XTable.vue:466-491 |
| Filter pruning | drops filters with no `value` unless `op ∈ {'null','nn'}` or `value == 0` | XTable.vue:469 |
| Page reset | if the filter+search hash changed since the last request → `query.page = 1` | XTable.vue:472-481 |
| Fetch | `await axios.post(this.model.url, this.requestData(op))`; on `op ∈ {'excel','json','print'}` → `window.open(data.url)` and return | XTable.vue:494-512 |
| Export buttons | json (line 96), excel (107), print (118) | XTable.vue:96-118 |
| Search | `a-input-search` → sets `query.search`, refetches | XTable.vue:10-15, 519-523 |
| Filter UI | dynamic rows via `XFilterRow`; add row pushes `{id: rand}` (XTable.vue:410-412); clear all disabled when empty (171) | XTable.vue:131-175 |
| Column selector | `visibleCols = cols.filter(c => c.show)` | XTable.vue:377-381 |
| Fullscreen | `openFullscreen`/`closeFullscreen` from `x-table/fullscreen.js` | XTable.vue:456-465 |
| Lifecycle | `mounted()` → `fetch()` | XTable.vue:384 |
Filter widgets (`x-table/components/`): `TextXTableFilter.vue`, `NumberXTableFilter.vue`, `DateXTableFilter.vue` (Jalali), `SelectOptionXTableFilter.vue`, `SwitchXTableFilter.vue`, `QuerySelectTableFilter.vue`, `XFilterRow.vue`.
### 9.2 Form inputs — `src/components/input/`
**`mixins.js`** — shared by all input components:
| Prop | Purpose |
|---|---|
| `field` | the `v-decorator` key |
| `items`, `form`, `formClass`, `label`, `help`, `show_help` | presentation |
| `initialValue` | merged into `_options.initialValue` (mixins.js:43-45) |
| `required` | when truthy, **auto-generates** `rules: [{required: true, message: \`وارد کردن ${label} الزامی است\`}]` (mixins.js:46-53) |
| `options` | raw ant decorator options, merged first (mixins.js:39-42) |
Computed `status` proxies `$refs.fi.getValidateStatus()` (mixins.js:29-34).
**`DateInput.vue`** (`name: 'JalaliInput'`) — Jalali date picker:
| Aspect | Detail | Line |
|---|---|---|
| Widget | `vue-persian-datetime-picker`, `color="#1c2d56"`, `clearable`, `auto-submit` | DateInput.vue:9-15 |
| Bound value | hidden `<input v-decorator="[field, _options]">` — the picker writes into the ant form imperatively | DateInput.vue:7 |
| Default format | `jYYYY/jMM/jDD` when no `format` prop | DateInput.vue:49 |
| Props | `format`, `displayFormat` | DateInput.vue:29-32 |
| Clear | `null` → sets the field to `null` via `$refs.fi.FormContext.form.setFieldsValue` | DateInput.vue:44-47 |
| Sync | `updated()` re-reads the field value back into the picker | DateInput.vue:57-59 |
**Uploaders**:
| Component | Upload target | Notes |
|---|---|---|
| `input/UploadInput.vue` | `baseURL + 'file/create'` (UploadInput.vue:72) | multi-file; a second definition in the same file uses `themeConfig.home('cu/file/create')` (line 215) — dead branch |
| `input/SingleUploadInput.vue` | `` `${baseURL}file/create` `` (SingleUploadInput.vue:52) | single file + preview modal |
| `input/SingleUploadInputPropList.vue` | same pattern | prop-driven list variant |
| `input/PodcastUploadInput.vue` | same pattern | audio |
| `x-upload/XUpload.vue` | `props.url`; `axios.post(url, formData, {'Content-Type':'multipart/form-data'})` | XUpload.vue:41-42, 69 |
| `components/MyUpload.vue`, `views/components/MyUpload.vue` | image cropper wrapper | |
| `excel/ImportExcel.vue` | client-side xlsx import | |
**`query-selector/QuerySelector.vue`** — debounced remote-search select:
| Aspect | Detail | Line |
|---|---|---|
| Props | `vDecorator`, `url` | QuerySelector.vue:42-45 |
| Debounce | 300 ms `setTimeout` (cancelled on each keystroke) | QuerySelector.vue:27-38 |
| Request | `axios.get(url, {params: {text_search: value}})` | QuerySelector.vue:34 |
| Option shape | `<a-select-option :key="d.id">{{ d.title }}</a-select-option>` | QuerySelector.vue:15-19 |
| Config | `show-search`, `:filter-option="false"` (server-side filtering), `:not-found-content="null"`, `:show-arrow="false"`, placeholder `جستجو` | QuerySelector.vue:2-14 |
### 9.3 Other components
| Component | Purpose |
|---|---|
| `tooltip-btn/TooltipBtn.vue` | Icon button + tooltip + optional `pop-title` confirm — the standard row-action control |
| `ZoomableImage.vue` | Click-to-zoom thumbnail (used in order/product detail modals) |
| `vx-card/VxCard.vue` | Page card wrapper — globally registered (globalComponents.js:3, 10) |
| `vx-list/VxList.vue`, `vx-input-group/VxInputGroup.vue`, `FeatherIcon.vue` | globally registered (globalComponents.js:11-14) |
| `vx-auto-suggest/VxAutoSuggest.vue` | Autocomplete (template) |
| `vx-table/VxTable.vue` | Legacy table (template) |
| `goldPrice.vue` | Live gold-price widget |
| `Moshtarian.vue` | Customer picker widget |
| `statistics-cards/` + `chartConfigs.js` | Dashboard stat cards |
| `timeline/VxTimeline.vue` | Timeline (template) |
| `icon/index.vue` + `icons.json` | Icon browser |
| `ChangeTimeDurationDropdown.vue` | Dashboard date-range picker |
| `VxTour.vue` | Product tour (disabled — `themeConfig.disableThemeTour = true`, themeConfig.js:20) |
| `vue-select` | registered globally with custom Deselect/OpenIndicator (globalComponents.js:18-40) |
---
## 10. Env / Config
### `auth_config.json`
```json
{ "domain": "YOUR_DOMAIN", "clientId": "YOUR_CLIENT_ID" }
```
Unfilled Auth0 placeholders. `src/plugins/auth.js` and `src/auth/authService.js` reference it, but `AuthPlugin` is **commented out** at `src/main.js:36-38`. **Dead config.**
### `themeConfig.js`
| Key | Value | Line |
|---|---|---|
| `colors.primary` | `#1c2d56` | themeConfig.js:6 |
| `colors.secondary` | `#836447` | themeConfig.js:7 |
| `colors.third` | `#f5cb55` | themeConfig.js:8 |
| `colors.success` / `danger` / `warning` / `dark` | `#28C76F` / `#EA5455` / `#FF9F43` / `#1E1E1E` | themeConfig.js:10-13 |
| `disableCustomizer` | `false` | themeConfig.js:19 |
| `disableThemeTour` | `true` | themeConfig.js:20 |
| `footerType` | `'hidden'` | themeConfig.js:21 |
| `hideScrollToTop` | `false` | themeConfig.js:22 |
| `mainLayoutType` | `isBranch() ? 'horizontal' : 'vertical'` | themeConfig.js:23 |
| `navbarColor` | `'#fff'` | themeConfig.js:24 |
| `navbarType` | `'floating'` | themeConfig.js:25 |
| `routerTransition` | `'zoom-fade'` | themeConfig.js:26 |
| **`rtl`** | `true` | themeConfig.js:27 |
| `sidebarCollapsed` | `false` | themeConfig.js:28 |
| `theme` | `'light'` | themeConfig.js:29 |
| `userInfoLocalStorageKey` | `'userInfo'` | themeConfig.js:31 |
Side effect: `Vue.use(Vuesax, {theme:{colors}, rtl: themeConfig.rtl})` at themeConfig.js:34.
### Environment variables
| File | Content |
|---|---|
| `.env.branch` | `NODE_ENV="branch"` (the only env file in the repo) |
`process.env` reads across the codebase:
| Reference | Location |
|---|---|
| `process.env.NODE_ENV == 'branch'` | src/utils/index.js:3 (the `isBranch()` switch) |
| `process.env.BASE_URL` | src/router.js:16 (router base) |
| `process.env.NODE_ENV` | src/store/store.js:59 (`strict` mode) |
| `process.env.NODE_ENV == 'branch'` | vue.config.js:3-4 (publicPath / outputDir) |
**No `VUE_APP_*` variables exist.** The API base URL is hard-coded at `src/axios.js:5`, and several table models and views re-hard-code the absolute `https://henzagold.com/oapi/admin/...` prefix rather than relying on `baseURL`.
### Other config files
| File | Notes |
|---|---|
| `babel.config.js` | preset `@vue/app` |
| `postcss.config.js` | tailwind + autoprefixer + postcss-rtl |
| `tailwind.config.js` (28 600 B) / `tailwind-v0.js` (26 430 B) | Tailwind theme; the `-v0` file is an unused backup |
| `.browserslistrc`, `.editorconfig`, `.eslintrc.js`, `.eslintignore` | tooling |
| `src/firebase/firebaseConfig.js` | imported only from a commented line (main.js:32-33) — dead |
| `src/i18n/i18n.js`, `i18nData.js` | vue-i18n set up (main.js:64-65); menu labels use a literal `i18n` field of Persian text rather than translation keys |
| `src/fake-db/*` | axios-mock-adapter fixtures; disabled (main.js:25-26) |
---
## Appendix A — Complete API endpoint index (as called from views / tables / components)
Relative paths resolve against `https://henzagold.com/oapi/admin/` (src/axios.js:5).
### Auth / user / roles
| Method | Endpoint | Caller |
|---|---|---|
| POST | `admin-login` | store/auth/moduleAuth.js:16 |
| GET | `users/show` | src/router.js:170 |
| POST | `/users` | views/user/UserCreate.vue:108 |
| POST | `/users/list` | views/notify/create.vue:128; src/table/userTbl.js:2 |
| PUT | `/users/{id}` | views/user/UserCreate.vue:104; views/user/ListUser.vue:162 |
| DELETE | `users/{id}` | views/user/ListUser.vue:224 |
| GET | `/user/list` | (listed in grep index) |
| GET | `/roles/{id}` | views/user/Permission.vue:136; VerticalNavMenu.vue:191 |
| POST | `roles/create` | views/user/Permission.vue:80, 93 |
| DELETE | `/roles/{ut}/{key}` | views/user/Permission.vue:86 |
### Products / variants / categories / brands
| Method | Endpoint | Caller |
|---|---|---|
| POST | `.../products` | table/product.js:20; views/order/product.js:3; views/invoice/AddInvoice.js:3; table/promotion_product.js:20 |
| POST | `product/create` | views/apps/product/AddProduct.vue:660 |
| PUT | `product/update/{id}` | AddProduct.vue:698; order/create.vue:341 |
| GET | `product/show/{id}` | AddProduct.vue:718; order/create.vue:360 |
| POST | `delete/product/{id}` | ProductList.vue:670; store/product/index.js:58 |
| POST | `get/variants` | ProductList.vue:631 |
| GET | `get/variant` | (grep index) |
| GET | `product/{id}/variant` | ProductList.vue:665 |
| POST | `add/variant` | order/create.vue:304; store/variant/index.js:51,54 |
| PUT | `product/variant/{id}` | store/product/index.js:115; store/promotion/index.js:94 |
| DELETE | `/product/variant/{id}` | store/variant/index.js:66 |
| POST | `product/variant` | store/cart/cart.js:73; store/invoiceCart/invoiceCart.js:68 |
| GET | `/product/color` | AddProduct.vue:857 |
| GET | `product/warranty/{id}` | ProductList.vue:556 |
| GET | `product/exist/{id}` | ProductList.vue:558 |
| GET | `/product/offer/{id}` | ProductList.vue:570 |
| POST | `/change/all` | ProductList.vue:582 |
| POST | `/change/percent/count` | views/apps/product/ChangePercent.vue |
| POST | `/product/report` | table/variant.js:3 |
| POST/GET | `category` | AddProduct.vue:976; CategoryTbl.js:3; store/category/* |
| DELETE | `category/{id}` | store/category/index.js:39 |
| PUT | `category/{id}` | store/category/index.js:52 |
| GET | `brand` | AddProduct.vue:986; table/brand.js:3 |
| POST | `/brand/create` | views/manage/Const.vue |
| PUT | `/brand/` | views/manage/Const.vue |
| DELETE | `/brand/` | views/manage/Const.vue |
| GET | `.../ciny` | AddProduct.vue:991 |
| POST/PUT/DELETE | `/ciny`, `/ciny/` | views/manage/Const.vue |
### Orders
| Method | Endpoint | Caller |
|---|---|---|
| POST | `.../single-order` | views/order/orderListModel.js:4 |
| POST | `.../major/order/table` | views/order/OmdeListModel.js:7 |
| POST | `.../order-tamin` | views/order/TaminListModel.js:3 |
| POST | `.../get-custom-orders` | views/order/makeOrderModel.js:4 |
| GET | `.../major/order` | views/customer/omde/omdeOrder.js:4 |
| GET | `.../single/order` | views/customer/single-customer/singleOrder.js:4 |
| PUT/POST | `order/`, `order/{id}` | store/order/index.js:54,57 |
| GET | `order/` | store/order/index.js:69 |
| DELETE | `order/{id}` | store/order/index.js:42 |
| GET | `/order/type` | views/order/create.vue:395 |
| POST | `single/order-sts/edit` | views/order/List.vue:260, 271 |
| POST | `order-sts/edit` | views/order/TaminList.vue |
| POST | `major/order-sts/edit` | views/order/OmdeList.vue |
| POST | `order-item/edit` | views/order/OmdeList.vue |
| DELETE | `major/order_item/` | views/order/OmdeList.vue |
| PUT | `custom-order/{id}` | views/order/MakeOrder.vue |
| DELETE | `custom-order/{id}` | views/order/MakeOrder.vue |
| POST | `delete-order` | views/order/* |
| *(window.open)* | `.../order/pdf/{id}` | views/order/List.vue:296 |
### Customers
| Method | Endpoint | Caller |
|---|---|---|
| POST | `.../customer` | views/customer/omde/OmdeListModel.js:7 |
| POST | `.../major/verify/table` | OmdeListNonVerifiedModel.js:7 |
| POST | `.../major/archive/table` | OmdeArchiveListModel.js:7 |
| POST | `.../banker` | BankerListModel.js:7 |
| POST | `.../shopkeeper` | shopkeeperListModel.js:7 |
| POST | `.../reagent` | ReagentListModel.js:7 |
| POST | `.../single-customer` | SingleCustomerList.js:3; views/order/create.vue:397 |
| POST | `.../major/table` | table/major_customer.js:3 |
| GET | `.../major/customer` | omdeCustomer.js:4 |
| POST | `customer/create` | store/customer/index.js:71 |
| GET | `customer/{id}` | bulkCustomer.vue:330; ReagentCustomer.vue:274 |
| POST | `customer/toggle-active` | omde list views |
| POST | `customer/toggle-sts` | omde list views |
| POST | `major/verify` | OmdeNonVerifiedList.vue |
| GET | `major/archive/{id}` | OmdeArchivedList.vue |
| GET | `major/dashboard/{id}` | ViewOmdeCustomer.vue |
| POST | `single` | singleCustomer.vue:92 |
| PUT | `single/{id}` | singleCustomer.vue:90 |
| GET | `single/{id}` | singleCustomer.vue:119 |
| GET | `single/order/{id}` / `single/report/{id}` / `single/wallet/{id}` | ViewSingleCustomer.vue |
| GET | `query/reagent` | bulkCustomer.vue:315 |
| GET/PUT/DELETE/POST | `user/profile[/{id}]` | store/customer/index.js:44,56,59,82,94 |
| GET | `user/profile/address/1` | (grep index) |
### Finance / documents / wallet / invoices
| Method | Endpoint | Caller |
|---|---|---|
| POST | `.../cash/table` | views/finical/list_model.js:3 |
| GET | `.../major/cash` | omdeTransaction.js:3 |
| GET | `.../single/transaction` | singleTransaction.js:3 |
| POST | `/wallet/table` | views/wallet/walletTbl.js:3 |
| GET | `/wallet/config` | views/manage/wallet.vue:77 |
| GET | `/wallet/birthday/config` | views/manage/wallet.vue:91 |
| POST | `/wallet/config` | views/manage/wallet.vue:109, 126 |
| POST | `.../branch/product/table` | views/invoice/ListInvoice.js:4; views/branch/orderListModel.js:4 |
| POST | `/branch/product` | views/invoice/AddInvoice.vue |
| GET | `branch/product/{id}` | views/invoice/EditInvoice.vue |
| DELETE | `branch/product/{id}` | views/invoice/ListInvoice.vue |
### Branches / workshops / cities
| Method | Endpoint | Caller |
|---|---|---|
| POST | `.../branch/table` | views/branch/ListBranch.js:3 |
| POST | `/branch` | views/branch/AddBranches.vue |
| PUT | `/branch/{id}` | views/branch/EditBranches.vue |
| GET | `/branch/{id}` | views/branch/EditBranches.vue |
| GET | `/branch/list` | (grep index) |
| GET | `/branch/active/{id}` | views/branch/ListBranches.vue |
| POST | `workshop/table` | views/workshop/ListWorkshop.js:3; views/order/workshopModel.js:3 |
| POST | `.../workshop/product/table` | views/workshop/orderListModel.js:4 |
| POST | `/workshop` | views/workshop/AddWorkshop.vue |
| PUT | `/workshop/{id}` | views/workshop/EditWorkshop.vue |
| GET | `/workshop/{id}` | views/workshop/EditWorkshop.vue |
| GET | `/workshop/active/{id}` | views/workshop/ListWorkshop.vue |
| POST | `cities/list` | store/users/index.js:52; bulkCustomer.vue:321; CityListModel.js:3 |
| POST | `cities` | views/city/createCity.vue |
| PUT | `cities/{id}` | views/city/cityList.vue |
| DELETE | `cities/{id}` | views/city/cityList.vue |
| GET | `cities/get-percent/{id}` | views/city/cityList.vue |
| GET | `cities/get-percent-aux/{id}` | views/city/cityList.vue; bulkCustomer.vue:265; ReagentCustomer.vue:221 |
### Promotions / offers
| Method | Endpoint | Caller |
|---|---|---|
| POST | `.../major/promotion/table` | src/table/list_model.js:3 |
| GET | `/product/variant/promotion/{id}` | views/promotion/* |
| POST/PUT/DELETE | `product/variant/promotion[/{id}]` | store/promotion/index.js:58,70,73,85 |
| POST/PUT/DELETE | `product/variant/promotion/variant-promotion[/{id}]` | store/variantpromotion/index.js:58,71,75,88 |
| GET | `/autopromotion` | views/promotion/autopromotion.vue |
| PUT | `/autopromotion` | views/promotion/autopromotion.vue |
| GET | `/ignore` | views/offersIgnore/offersIgnore-add-edit.vue |
| POST | `/ignore` | views/offersIgnore/offersIgnore-add-edit.vue |
| DELETE | `/ignore/` | views/offersIgnore/offersIgnore-add-edit.vue |
### Comments / SMS / notifications / newsletter / reports / config
| Method | Endpoint | Caller |
|---|---|---|
| POST | `.../comment/table` | views/customer/Comments/CommentListModel.js:4 |
| POST | `comment` | Comments.vue |
| POST | `comment/sts` | Comments.vue |
| DELETE | `comment/{id}` | Comments.vue |
| POST | `/sms/table` | views/sms/create.vue:168 |
| POST | `https://henzagold.com/oapi/api/sms/batch` | views/sms/create.vue:216, 231 |
| POST | `https://henzagold.com/oapi/api/sms/batch/test` | views/sms/create.vue:195 |
| GET | `.../major/sms` | omdeSms.js:3 |
| GET | `.../single/sms` | singleSms.js:3 |
| POST | `/notify` | views/notify/create.vue:112 |
| POST | `/notify/table` | views/notify/notifyTbl.js:3 |
| POST | `/notify/customer` | views/notify/create.vue:139 |
| GET | `/notify/{id}` | views/notify/user.vue |
| GET | `/notify/read/{id}` | views/notify/user.vue |
| POST | `.../fashion/table` | views/newsletters/newsletter.js:3 |
| POST | `.../report/order_item` | views/report/order_item_model.js:3 |
| GET | `get-analytics` | views/DashboardECommerce.vue |
| GET | `get-analytics-tak` | views/DashboardAnalytics.vue |
| GET | `/dashboard/link` | views/manage/report.vue:52 |
| POST | `log/{mode}`, `log/customer/{id}` | views/statics/index.vue:147 |
| GET | `log/ago/{id}` | views/statics/index.vue:174 |
| GET | `/log/list/customer` | views/statics/index.vue:188 |
| GET | `config/statics` | store/{product,variant,promotion,variantpromotion,manage}/*.js |
| GET | `/const/__list__ut` | views/user/Permission.vue:113; UserCreate.vue:169; ListUser.vue:257 |
| GET | `/const/__list__tags` | AddProduct.vue:989 |
| GET | `/const/mobile_main_slider`, `/const/site` | views/manage/Const.vue |
| POST | `const`, `/const` | views/user/Permission.vue:77; views/manage/Const.vue |
| PUT | `/const/`, `/const/49`, `/const/54`, `/const/site_test` | views/manage/Const.vue |
| DELETE | `/const/by-id/` | views/manage/Const.vue |
| GET | `price` | components/goldPrice.vue |
| PUT | `file/update/` | upload components |
| POST | `{baseURL}file/create` | input/UploadInput.vue:72; input/SingleUploadInput.vue:52 |
| GET | `/te/query`, `POST /ddf` | scratch/debug endpoints |
---
## Appendix B — Known defects / oddities found while reading
| # | Issue | Evidence |
|---|---|---|
| 1 | No response interceptor and no token-refresh in the live app; the whole `src/http/requests/**` refresh machinery is unreachable | src/axios.js (no response interceptor); src/main.js:22-23 (import commented) |
| 2 | Request-error interceptor does not return the rejection | src/axios.js:29-31 |
| 3 | `store.state.auth.user` / `.token` are **functions**, so `isLogin` in the guard is always truthy | store/auth/moduleAuth.js:5-10 vs router.js:164 |
| 4 | Header-click sorting inverts direction relative to the sort dropdown (`ascend` → `-`) | XTable.vue:449 vs XTable.vue:342 |
| 5 | `XTbl.hideSearch()` and `addExtraBtn()` both set `_hidePageination` (copy-paste) | XTbl.js:51-59 |
| 6 | Menu URL `/customer/Shopkeeper-list` does not match route `/customer/shopkeeper-list` (case) | navMenuItems.js:90 vs admin.js:550 |
| 7 | `order/List.vue` "ویرایش سفارش" persists nothing — the `axios.put` is commented out | views/order/List.vue:281 |
| 8 | Retail order-status enum lacks `0: معلق` present in the wholesale filter | create.vue:158 vs OmdeListModel.js:23 |
| 9 | Banker / Shopkeeper menu entries have no `permissionId`, so they render for everyone | navMenuItems.js:86-87, 93-94 |
| 10 | Permission keys `persons.create.customer.tak` and `persons.list.sellers.view` are referenced in views but absent from `permissionConst.js` | AddCustomer.vue:142; ReagentList.vue:32 |
| 11 | `/newsletters` route has its `role` commented out → reachable by any authenticated admin | admin.js:851 |
| 12 | Jalali datetime filter uses 12-hour `hh` with no am/pm marker | filters.js:109 |
| 13 | Base URL is hard-coded in `src/axios.js` and again as absolute strings in ~30 table models | axios.js:5; e.g. orderListModel.js:4 |
| 14 | SMS send bypasses the admin base and posts to `/oapi/api/sms/batch` | views/sms/create.vue:216 |
| 15 | Refresh-token payload reads `localStorage.getItem('accessToKen')` (capital K typo) | http/requests/auth/jwt/index.js:61 |
| 16 | `auth_config.json` still holds Auth0 placeholders and is never used | auth_config.json; main.js:36-38 |
| 17 | No gram/weight formatter, no Persian↔English digit conversion, no price input mask anywhere | absent from `src/filters`, `src/utils`, `src/components/input` |
| 18 | Permission deletion in the role editor fires N staggered `setTimeout(…, 500)` deletes — racy for large diffs | views/user/Permission.vue:86 |