Files
tehran_taj/docs/parity/_inv-ref-admin.md
T

1706 lines
127 KiB
Markdown
Raw Normal View History

# 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 |