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

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

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

[ref: henza_server, henza_admin, henza_pwa]

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

1179 lines
102 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Inventory — henza_pwa (reference user app)
> Scope: `E:\gold bank\henza_pwa`. All `path:line` are relative to `henza_pwa/`.
> Excluded from the file census: `node_modules`, `dist`, `3.0.0`, and two **vendored** libraries checked into `src/components/`: `src/components/DatePicker/DatePicker/**` (forked ant-design-vue `vc-picker` + Jalali locale) and `src/components/JalaliAntDatePicker/**` (forked ant-design-vue `_util`/components). Those are third-party code, not app code.
>
> **Critical framing finding:** this app is **NOT** a consumer "gold bank" (buy gram / sell gram / wallet / deposit / withdraw / transfer / KYC-level gating). It is a **B2B wholesale gold jewelry catalog + ordering app** for jewelers (`مشتری`/`کارشناس فروش`/`بنکدار`). There is **no buy-gold, sell-gold, wallet, deposit, withdraw, transfer, cart-payment, address-book, ticket/support, or KYC-level system** anywhere in the codebase (verified by exhaustive grep of every axios call — see §2.4). The closest analogues are documented per-section and flagged **ABSENT** where the requested feature does not exist.
---
## 1. Stack & versions
Source: `package.json:1-58`.
| Concern | Package | Version | Evidence |
|---|---|---|---|
| Framework | `vue` | `^3.2.29` | `package.json:34` |
| Build | `vite` | `^3.2.5` | `package.json:52` |
| Vue plugin | `@vitejs/plugin-vue` | `^2.0.0` | `package.json:42` |
| JSX plugin | `@vitejs/plugin-vue-jsx` | `^3.0.0` | `package.json:43` |
| UI library (primary) | `ant-design-vue` | `^3.2.20` | `package.json:17` |
| UI library (mobile) | `vant` | `^4.0.3` | `package.json:33` |
| Icons | `@ant-design/icons-vue` `^6.1.0`, `feather-icons` `^4.28.0`, `vue-feather` `^2.0.0-rc.1` | — | `package.json:10,21,35` |
| Router | `vue-router` | `^4.0.12` | `package.json:36` |
| Store (Pinia) | `pinia` | `^2.1.4` | `package.json:30` — **only used by one store** (`src/views/report/store/report.ts:9`). All other state is hand-rolled class singletons. |
| HTTP client | `axios` | `^0.25.0` | `package.json:18` |
| Date (Jalali) | `moment-jalaali` `^0.10.0`, `jalaliday` `^2.3.0`, `jalali-dayjs` `0.0.1`, `dayjs` `^1.11.6`, `date-fns` `^2.29.3` | — | `package.json:19,20,24,25,29` |
| Date picker | `vue3-persian-datetime-picker` `^1.2.2` (+ vendored fork under `src/components/DatePicker/`) | — | `package.json:38` |
| PWA plugin | `vite-plugin-pwa` | `^0.11.13` | `package.json:54` |
| Service-worker toolkit | `workbox-precaching` | `^6.4.2` | `package.json:56` |
| **Socket / live-price lib** | **NONE** | — | No `socket.io`, `ws`, `pusher`, `centrifuge`, `sse`, `eventsource` in `package.json`. Live price is **manual pull only** — see §10. |
| Push notifications | `firebase` `^9.6.7`, `@firebase/app` `^0.7.17`, `@firebase/messaging` `^0.9.8` | — | `package.json:11,12,22` — FCM wiring exists but is **disabled** (`src/App.vue:104-106` `useFirebase()` commented out) |
| Animation | `gsap` `^3.9.1` | — | `package.json:23` (used by bottom-nav morph, `src/components/ui/navigation.vue:83`) |
| Lottie | `@lottiefiles/dotlottie-vue` `^0.4.15` | — | `package.json:14` (version-update dialog, `src/App.vue:24`) |
| Carousel | `swiper` `^7.4.1` | — | `package.json:32` |
| Lazy images | `vue3-lazyload` `^0.2.5-beta` | — | `package.json:37` (registered `src/main.ts:32-46`) |
| Utils / composables | `lodash` `^4.17.21`, `@vueuse/core` `^9.13.0` | — | `package.json:28,16` |
| CSS | `tailwindcss` `3.0.18`, `less` `^4.1.2`, `postcss-rtl` `^1.7.3`, `autoprefixer`, `@fullhuman/postcss-purgecss` | — | `package.json:13,26,31,44,50` |
| TS | `typescript` `^4.4.4`, `vue-tsc` `^0.29.8` | — | `package.json:51,55` |
| App version | `1.0.7` | — | `package.json:3` (compared against `localStorage.version` at `src/App.vue:57-68`) |
---
## 2. HTTP layer
### 2.1 Base URLs & config
`src/service/` contains a **single** file (`src/service/hesab.ts`) with 5 functions. **Every other API call is inlined in views/stores/mixins.** Full census in §2.4.
| Key | Value | Evidence |
|---|---|---|
| `$themeConfig.baseURL` | `https://henzagold.com/oapi/api/` | `themeConfig/themeConfig.ts:43` |
| `$themeConfig.coreURL` | `https://henzagold.com/oapi/` | `themeConfig/themeConfig.ts:45` |
| `baseURL` (named export, unused by axios) | `https://henzagold.com/oapi/api/` | `themeConfig/themeConfig.ts:40` |
| `$themeConfig.baseRoute` | `/` | `themeConfig/themeConfig.ts:47` |
| `$themeConfig.outDir` | `dist` | `themeConfig/themeConfig.ts:48` |
| `$BasePath` | dev → `http://localhost:3030`, prod → `https://app.henzagold.com` | `themeConfig/themeConfig.ts:50` |
| Commented-out local alternatives | `http://localhost/api/`, `http://localhost/oapi/` | `themeConfig/themeConfig.ts:44,46` |
There is **no `.env` / `import.meta.env` usage anywhere** — all config is hard-coded in `themeConfig/themeConfig.ts`. `vite.config.ts:91-93` stubs `process.env` to `{}`.
### 2.2 Axios instances (`src/plugins/axios.ts`)
Three instances, all created with the same static default `headers` object built **once at module load** (`src/plugins/axios.ts:6-10`):
```
Content-Type: application/json
accept: application/json
Authorization: Bearer ${getUserToken()} // snapshot at import time
```
| Export | baseURL | Request interceptor | Evidence |
|---|---|---|---|
| `default` (`ax`, imported everywhere as `@axios`) | `$themeConfig.baseURL` | Overwrites `config.headers.Authorization = 'Bearer ' + getUserToken()` on **every** request | `src/plugins/axios.ts:15-24` |
| `noHeaderAxios` | `$themeConfig.baseURL` | Pass-through (`config => config`) — caller must set `Authorization` manually | `src/plugins/axios.ts:27-32` |
| `cax` | `$themeConfig.coreURL` | Sets `Authorization = 'Bearer ' + localStorage.getItem('appToken')` (ignores child token) | `src/plugins/axios.ts:37-45` — **`cax` is never imported anywhere** (dead export) |
`@axios` alias → `src/plugins/axios` (`vite.config.ts:61-64`). `@themeConfig` → `themeConfig/themeConfig` (`vite.config.ts:57-60`). `/@/` → `src/` (`vite.config.ts:65-68`).
### 2.3 Interceptors, token, 401, error toasts
| Aspect | Reality | Evidence |
|---|---|---|
| **Request interceptor** | Only attaches `Authorization`. Error handler is a **no-op bug**: `error => { Promise.reject(error) }` — result not returned, so the promise resolves `undefined`. | `src/plugins/axios.ts:17-23`, same bug at `:29-31` and `:39-45` |
| **Response interceptor** | **NONE.** No response interceptor is registered on any instance. | `src/plugins/axios.ts` (entire file, 46 lines) |
| **Token source** | `getUserToken() = userChildToken() ?? userToken()` | `src/store/tokenManage.ts:8-10` |
| `userToken()` | `localStorage.getItem('appToken') \|\| localStorage.appToken \|\| null` | `src/store/tokenManage.ts:4-6` |
| `userChildToken()` | `localStorage.getItem('activeChildToken') \|\| localStorage.activeChildToken \|\| null` | `src/store/tokenManage.ts:1-3` |
| **Impersonation** | If a sales-rep selects a customer, that customer's `api_token` is stored as `activeChildToken` and **takes priority** over the rep's own token for all requests. | `src/store/myDataStore.ts:159-169` |
| **Refresh logic** | **ABSENT.** No refresh token, no retry queue, no `/refresh` endpoint anywhere. | verified by grep — no `refresh` API call exists |
| **401 handling** | Handled in **exactly one place**, `myStore.init()`: on `e.response.status === 401` → remove `appToken`, `appCart`, `activeChildToken`; `toggleActiveUser(undefined)`; `router.push({name:'home'})`; then `throw e`. | `src/store/myDataStore.ts:134-145` |
| **403 handling** | **ABSENT.** No `403` reference anywhere. | grep |
| **Global error toast** | **ABSENT.** Every call site does its own `try/catch { console.log(e) }`. Toasts are per-view (`showNotify` / `NotifyVant` / `message.error`). | e.g. `src/views/home/index.vue:218-222`, `src/views/cart.vue:189-193`, `src/Mixins/getProducts.ts:35-37` |
| **Per-call error toasts that DO exist** | `addUser` duplicate-mobile (`src/views/user/src/components/addUser.vue:182-189`), upload errors (`src/components/SingleImageUploader.vue:94-97,117-122,129-139`), login 404/other (`src/views/user/LoginRegister/components/Login.vue:147-154,177-183`), makeOrder (`src/views/user/src/components/makeOrder.vue:224,229`), editUser success (`src/views/user/src/components/editUser.vue:285-291`) | — |
### 2.4 Complete API function census
Every network call in the app (excluding the two vendored libs). **58 distinct call sites, 44 distinct endpoints.**
#### `src/service/hesab.ts` (the only "service" file — 5 functions)
| # | Function | Method | URL | Params / body | Return type | Evidence |
|---|---|---|---|---|---|---|
| 1 | `hesabDetail(page=1, limit=10)` | POST | `/hesab_detail/table` | body `{limit, page}` | `TableOutput<Hesab>` | `src/service/hesab.ts:12-14` — **never called** (only imported at `src/views/report/index.vue:74`, call commented out at `:215`) |
| 2 | `hesabDetailNew(page=1, limit=10)` | GET | `/fin_tahesab/table` | **params ignored** (signature takes them, URL uses none) | `TableOutput<Accounting>` | `src/service/hesab.ts:16-18` |
| 3 | `accountingDetail(factor_code?)` | GET | `/fin_tahesab/detail/${factor_code}` | path param | `TableOutput<Accounting>` | `src/service/hesab.ts:20-22` |
| 4 | `accountingDetailList(offset?)` | GET | `/fin_tahesab/my_detail/${offset}` | path param | — | `src/service/hesab.ts:23-25` |
| 5 | `hesabMe()` | GET | `/tahesab/me` | — | `any` | `src/service/hesab.ts:26-28` |
#### Auth & profile
| # | Method | URL | Body / params | Called from | Evidence |
|---|---|---|---|---|---|
| 6 | POST | `/web/auth` | `{username}` (phone) | Login step 1 | `src/views/user/LoginRegister/components/Login.vue:137` |
| 7 | POST | `/user-verfiy` *(sic — typo in endpoint)* | `{username, password /*=OTP*/, token_mobile}` | Login step 2 | `src/views/user/LoginRegister/components/Login.vue:165` |
| 8 | POST | `/major/register` | `{fname, lname, mobile, store_name, phone, address, city}` | Register | `src/views/user/LoginRegister/components/Register.vue:332` |
| 9 | POST | `/major/complete` | `{business_license_image_id, shop_image_id, ncode_image_id}` | Complete profile (doc upload) | `src/views/user/src/completeProfile.vue:193` |
| 10 | POST | `/me-data` | `{}` | `myStore.init()` — **called twice**: once via `noHeaderAxios` with explicit parent token, once via `axios` with child token | `src/store/myDataStore.ts:120-126` (noHeaderAxios), `:130` |
| 11 | POST | `/me-data` | `{}` | makeOrderStatus bootstrap | `src/views/user/src/components/makeOrderStatus.vue:180` |
| 12 | POST | `/update-profile` | `{image_id, phone, address, fname, lname, mobile, birth_date, marriage_date}` | editUser | `src/views/user/src/components/editUser.vue:289` |
| 13 | POST | `/cities/list` | `{}` → `{rows: City[]}` (flat list w/ `level`,`parent`) | Register city cascader | `src/views/user/LoginRegister/components/Register.vue:299` |
#### Catalog / products
| # | Method | URL | Body / params | Called from | Evidence |
|---|---|---|---|---|---|
| 14 | POST | `/front` | `{}` → `{products, more, cats, ciny}` | Home bootstrap | `src/views/home/index.vue:184` |
| 15 | POST | `/front` | `{}` (for `ciny` fallback list) | `getCollection` in products mixin | `src/Mixins/getProducts.ts:46` |
| 16 | POST | `/get-products` | full `Query` object (see §8) → `{rows, count, limit}` | product list / search / newest | `src/Mixins/getProducts.ts:28` |
| 17 | POST | `/get-products` | `{products_id: number[]}` → `{rows}` | cart re-hydration | `src/store/cartStore.ts:41` |
| 18 | GET | `/ciny/${cats[0]}` | path param = first category id | collection list for a category | `src/Mixins/getProducts.ts:42` |
| 19 | GET | `/product/${id}` | path param (from route) | single product | `src/views/singleProduct.vue:283` |
| 20 | GET | `/product/${props.id}` | path param (from prop, drawer mode) | single product in drawer | `src/views/singleProduct.vue:304` |
| 21 | GET | `/config` | — → `{about, main_slider}` | base store init | `src/store/store.ts:63` |
| 22 | GET | `/promotion` | — (only if logged in) | base store init | `src/store/store.ts:74` |
| 23 | GET | `/ignore` | — (only if logged in) | base store init | `src/store/store.ts:86` |
| 24 | GET | `/ignore` | — | `getCustomerListType()` — **dead**, never called, duplicates #23 | `src/store/store.ts:98` |
| 25 | GET | `/price` | — → keyed object of price rows | base store `getPriceList()` | `src/store/store.ts:114` |
| 26 | GET | `/price` | — → array or object | Dashboard `getListPrice()` (separate, newer path) | `src/views/user/src/components/Dashboard.vue:634` |
#### Favorites
| # | Method | URL | Body | Called from | Evidence |
|---|---|---|---|---|---|
| 27 | POST | `/add-to-fav` | `{id}` — **toggle** (optimistic local toggle first) | favorite mixin | `src/Mixins/favorite.ts:8` |
| 28 | POST | `/get-favorites` | `{page}` → `{rows}` | favorites list | `src/views/user/src/components/favorits.vue:52` |
#### Orders (catalog orders)
| # | Method | URL | Body | Called from | Evidence |
|---|---|---|---|---|---|
| 29 | POST | `/set-order` | `{final_submit:false, from_parent:boolean, id:null, items:[{discount, id:null, note, percent_pay, product, promotion_id, promotion_item_id, qty, rash_weight, rial_pay:0, variant, wage_weight, weight}]}` → `{id}` | cart checkout | `src/views/cart.vue:161-185` |
| 30 | POST | `/get-orders` | `{sts}` → `{rows}` (`sts = -1` for all) | orders list / tab change | `src/views/user/src/components/Orders.vue:290` |
| 31 | POST | `/order/child` | `{page, search}` → `{rows}` | sales-rep: customers' orders | `src/views/user/src/components/ChildOrders.vue:332` |
| 32 | PUT | `/order/child/${id}` | `{items:[...w, rash_weight], final_submit, id, from_parent:true}` | sales-rep: edit/finalize a customer order | `src/views/user/src/components/ChildOrders.vue:400` |
#### Custom manufacture orders ("سفارش ساخت")
| # | Method | URL | Body | Called from | Evidence |
|---|---|---|---|---|---|
| 33 | POST | `/create-custom-order` | `{color, size, color_mina, color_stone, sum, file_id, description}` | makeOrder submit | `src/views/user/src/components/makeOrder.vue:221` |
| 34 | GET | `/get-custom-orders` | — → `{rows}` | makeOrderStatus list | `src/views/user/src/components/makeOrderStatus.vue:187` |
| 35 | POST | `/update-custom-order/${orderId}` | `{sts: 2}` | (commented-out generic updater) | `src/views/user/src/components/makeOrderStatus.vue:197` |
| 36 | POST | `/update-custom-order/${orderId}` | `{sts: 2}` | `confirmOrder` | `src/views/user/src/components/makeOrderStatus.vue:212` |
| 37 | POST | `/update-custom-order/${orderId}` | `{sts: 2}` | `completeOrder` — **same `sts:2` as confirm (likely bug)** | `src/views/user/src/components/makeOrderStatus.vue:234` |
#### Financial documents ("اسناد" / findoc)
| # | Method | URL | Body | Called from | Evidence |
|---|---|---|---|---|---|
| 38 | POST | `/get-my-findoc` | `{}` → `{rows}` — **no pagination, no filters** | transactions list | `src/views/user/src/components/transactions.vue:203` |
| 39 | POST | `/set-findoc` | type-dependent (see §6.9) | addTransaction (page) | `src/views/user/src/components/addTransaction.vue:196` |
| 40 | POST | `/set-findoc` | type-dependent (identical) | addFindoc (drawer, duplicate) | `src/views/user/src/components/addFindoc.vue:204` |
#### Customers (sales-rep only)
| # | Method | URL | Body | Called from | Evidence |
|---|---|---|---|---|---|
| 41 | POST | `/my-child` | `{}` → customer array | customerChoice list | `src/views/user/src/components/customerChoice.vue:184` |
| 42 | POST | `/customer/toggle-active` | `{id}` | customerChoice activate/deactivate | `src/views/user/src/components/customerChoice.vue:199` |
| 43 | POST | `/customer/toggle-active` | `{id}` | Dashboard `setActive` — **dead** (`getCustomers` is undefined in that scope) | `src/views/user/src/components/Dashboard.vue:593` |
| 44 | POST | `/customer/create` | full `userFormModel` | addUser (create mode) | `src/views/user/src/components/addUser.vue:417` |
| 45 | POST | `/customer/update` | `{...userFormModel, id: route.query.edit}` | addUser (edit mode) | `src/views/user/src/components/addUser.vue:406` |
| 46 | GET | `https://henzagold.com/oapi/admin/major/list_type` | **absolute URL, bypasses baseURL, hits `/admin/`** | addUser: customer-type dropdown | `src/views/user/src/components/addUser.vue:457-460` |
#### Notifications
| # | Method | URL | Body | Called from | Evidence |
|---|---|---|---|---|---|
| 47 | POST | `get-notify` *(no leading slash)* | `{page}` → `{rows}` | notification list (infinite scroll) | `src/views/home/src/components/NotificationList.vue:101` |
| 48 | GET | `/notify/count` | — → number | unread badge | `src/views/home/src/components/NotificationList.vue:117` |
| 49 | PUT | `notify/read/${id}` *(no leading slash)* | — | mark read | `src/views/home/src/components/NotificationList.vue:132` |
> `NotificationList.vue` is **not mounted** — its usage in Home is commented out (`src/views/home/index.vue:5`).
#### Files
| # | Method | URL | Body | Called from | Evidence |
|---|---|---|---|---|---|
| 50 | POST | `https://henzagold.com/oapi/api/file/create` | `FormData{file, has_thumb:'0', has_watermark:'0'}`, header `Content-Type: multipart/form-data` → `{id, image}` | SingleImageUploader | `src/components/SingleImageUploader.vue:107-109` |
#### Analytics (fires on every route change)
| # | Method | URL | Body | Called from | Evidence |
|---|---|---|---|---|---|
| 51 | POST | `/log/create/${key}` | `{}` or `{data:{product_id}}` when `to.name === 'singleProduct'`; `key = meta.title` + `-${params.id}` | router `beforeEach` | `src/router/customAnalytics.ts:26` |
| 52 | GET | `/log/update/${lastRequestReturnedId}` | — (closes previous page-view) | router `beforeEach` | `src/router/customAnalytics.ts:17` |
| 53 | POST | `/log/create/${routeName}` | — | `useCustomAnalyticsComponent` composable, `onBeforeMount` | `src/router/customAnalytics.ts:40` |
| 54 | GET | `/log/update/${requestReturnedId}` | — | same composable, `onBeforeUnmount` | `src/router/customAnalytics.ts:53` |
Excluded routes for auto-analytics: `['login', 'userPage::hasSubRoute']` (`src/router/customAnalytics.ts:6`).
#### Non-axios external URLs (`window.open`)
| Purpose | URL | Evidence |
|---|---|---|
| Order invoice PDF | `https://henzagold.com/oapi/api/report/order?order_id=${id}` (target `_bank`) | `src/views/user/src/components/Orders.vue:334` |
| Findoc file | `https://henzagold.com/oapi/api/report/doc?doc_id=${id}` (target `_bank`) | `src/views/user/src/components/transactions.vue:225` |
| Phone call | `tel:${mobile}` | `src/views/user/src/components/profile.vue:149-151` (line numbering within `<script setup>`) |
| Price row deep-link | `window.open(val.link)` when a price row carries `link` | `src/views/user/src/components/Dashboard.vue:453-455,244` |
| WhatsApp support (static FAB) | `https://wa.me/989229425856` | `index.html:59-61` |
### 2.5 ABSENT endpoints (explicitly verified not to exist)
`buy`, `sell`, `wallet`, `balance` (as an endpoint), `deposit`, `withdraw`, `transfer`, `payment`, `gateway`, `ticket`, `support`, `address`, `kyc`, `verify-level`, `refresh`, `logout` (client-side only). Verified by full-repo grep of every `axios.*` call — the 54-item table above is exhaustive.
---
## 3. Router — every route
File: `src/router/index.ts`. History mode: `createWebHistory($themeConfig.baseRoute)` = `createWebHistory('/')` (`src/router/index.ts:224`). `scrollBehavior`: restore `savedPosition` else `{top:0}` (`src/router/index.ts:226-238`).
**Route count: 22** (21 real + 1 catch-all redirect).
| # | path | name | component file | meta.layout | meta.title | meta.skipLoadedCheck | auth guard? | KYC level req? | Evidence |
|---|---|---|---|---|---|---|---|---|---|
| 1 | `/home` | `home` | `src/views/home/index.vue` | — (Master) | `صفحه-اصلی` | — | public | none | `src/router/index.ts:30-38` |
| 2 | `/collection/:id?` | `collection` | `src/views/search/collection/index.vue` | — (Master) | `سینی` | — | public | none | `:39-46` |
| 3 | `/search/:id?` | `search` | `src/views/search/searchPage.vue` | — (Master) | `جستجو` | — | public | none | `:47-54` |
| 4 | `/product/:id` | `singleProduct` | `src/views/singleProduct.vue` | `FullPage` | `تک-محصول` | — | public | none (add-to-cart gated in-view) | `:55-63` |
| 5 | `/newest` | `newestProduct` | `src/views/newest.vue` | `FullPage` | **none** | — | public | none | `:64-71` |
| 6 | `/user` | `userPage::hasSubRoute` | `src/views/user/index.vue` | — (Master) | `کاربر` | — | public (shows login CTA when logged out) | none | `:72-79` |
| 7 | `/orders` | `ordersList` | `src/views/user/src/components/Orders.vue` | — (Master) | `سفارشات` | — | **AUTH** → home + notify | none | `:80-87`, guard `:257-261` |
| 8 | `/transactions` | `transactionsList` | `src/views/user/src/components/transactions.vue` | — (Master) | `اسناد` | — | **AUTH** → home + notify | none | `:89-96`, guard `:257` |
| 9 | `/addTransaction` | `addTransaction` | `src/views/user/src/components/addTransaction.vue` | — (Master) | `افزودن سند` | — | *not* in guard list (reachable logged-out) | none | `:97-104` |
| 10 | `/profile` | `profileData` | `src/views/user/src/components/profile.vue` | — (Master) | `پروفایل` | — | **AUTH** → home + notify | none | `:105-112`, guard `:257` |
| 11 | `/makeOrder` | `makeOrderList` | `src/views/user/src/components/makeOrder.vue` | — (Master) | `ساخت سفارش` | — | *not* in guard list | none | `:113-120` |
| 12 | `/makeOrder_status` | `makeOrderStatus` | `src/views/user/src/components/makeOrderStatus.vue` | — (Master) | `وضعیت سفارش` | — | *not* in guard list | none | `:121-128` |
| 13 | `/favorites` | `favoritesList` | `src/views/user/src/components/favorits.vue` | — (Master) | `علاقمندی` | — | **AUTH** → home + notify | none | `:129-136`, guard `:257` |
| 14 | `/addUser` | `addUser` | `src/views/user/src/components/addUser.vue` | — (Master) | `افزودن کاربر` | **true** | *not* in guard list | none (sales-rep only by UI) | `:137-145` |
| 15 | `/editUser` | `editUser` | `src/views/user/src/components/editUser.vue` | — (Master) | `ویرایش کاربر` | — | *not* in guard list | none | `:146-153` |
| 16 | `/cart` | `cart` | `src/views/cart.vue` | — (Master) | `سبد-خرید` | — | public | none | `:154-161` |
| 17 | `/report` | `report` | `src/views/report/index.vue` | `FullPage` | `گزارش` | — | *not* in guard list; **in-view redirect if no `mcode`** | none | `:162-170`, in-view `src/views/report/index.vue:174-177` |
| 18 | `/ChildOrder` | `ChildOrder` | `src/views/user/src/components/ChildOrders.vue` | `FullPage` | `سفارش مشتریان` | — | *not* in guard list (sales-rep only by UI) | none | `:171-179` |
| 19 | `/customerChoice` | `customerChoice` | `src/views/user/src/components/customerChoice.vue` | `FullPage` | `انتخاب مشتری` | **true** | *not* in guard list | none | `:180-189` |
| 20 | `/report/detail` | `reportDetail` | `src/views/report/detail.vue` | `FullPage` | `جزئیات گزارش` | — (`props: true`) | *not* in guard list; **in-view redirect if no `currentReport`** | none | `:190-199`, in-view `src/views/report/detail.vue:126-131` |
| 21 | `/login` | `login` | `src/views/user/LoginRegister/index.vue` | `FullPage` | **none** | — | **reverse guard**: logged-in → home | none | `:200-207`, guard `:262-265` |
| 22 | `/complete_profile` | `completeProfile` | `src/views/user/src/completeProfile.vue` | `FullPage` | **none** | — | **AUTH** → home + notify; **AND** if `sts_verify_step_two ∈ {1,2}` → home | *closest thing to a KYC gate* | `:208-215`, guards `:257`, `:266` |
| 23 | `/:pathMatch(.*)` | `notExistPages` | — (redirect to `home`) | — | — | — | — | — | `:216-220` |
Route meta typing is declared at `src/router/index.ts:18-24`: `{layout?: string; transition?: string; skipLoadedCheck?: boolean}`. Note `title` is used but **not declared** in the interface.
**Routes with no `meta.title`** (5, 21, 22) break `useCustomAnalytics`, which does `let key = toRoute.meta.title` (`src/router/customAnalytics.ts:19`) → posts `/log/create/undefined`.
---
## 4. Navigation (bottom nav)
Definition: `src/store/navStore.ts:34-48`. Renderer: `src/components/ui/navigation.vue`. Mounted only in the `Master` layout (`src/layout/master.vue:29`), so `FullPage` routes have no bottom nav.
| index | name (route) | path | icon | gating | Evidence |
|---|---|---|---|---|---|
| 1 | `userPage::hasSubRoute` | `/user` | `vuesax-linear:user` | none | `src/store/navStore.ts:40` |
| 2 | `cart` | `/cart` | `vuesax-linear:bag-happy` | none | `:41` |
| 3 | `search` | `/search` | `vuesax-linear:search-normal-1` | none | `:42` |
| 4 | `home` | `/` | `vuesax-linear:home-2` | none | `:43` |
Defaults: `activeLink: 4`, `showNav: true`, `pageTransition: 'right'` (`src/store/navStore.ts:45-47`). A commented-out earlier feather-icon variant sits at `:36-39`.
**Visibility rule** (`src/router/index.ts:281-285`, `afterEach`): if `to.name` matches a nav item → `setActiveLink(item.index)` + `setNavVisibility(true)`; otherwise → `setNavVisibility(false)`. So the bar is visible **only** on `/user`, `/cart`, `/search`, `/home`.
Also hidden while the search filter panel is open: `showFilters()` → `nav.setNavVisibility(false)`, `hideFilters()` → `true` (`src/views/search/searchPage.vue:152-165`).
**Visual behaviour:** an SVG path morphs a "notch" under the active item, animated with GSAP (`src/components/ui/navigation.vue:50-86`), and the active icon translates via `useTransition` with `TransitionPresets.easeInOutCubic`, 150 ms (`:110-117`). Note the active icon is rendered by 4 hard-coded `v-if` comparisons on the icon string rather than a dynamic bind (`:15-18`).
**Dashboard in-page tile grid** (a second nav surface), `src/views/user/src/components/Dashboard.vue:721-743` + dispatcher `:749-772`:
| id | icon | title | target route |
|---|---|---|---|
| 0 | `vuesax-linear:box` | سفارشات | `ordersList` |
| 1 | `vuesax-linear:receipt-text` | اسناد | `transactionsList` |
| 2 | `vuesax-linear:user` | پروفایل | `profileData` |
| 3 | `vuesax-linear:heart` | علاقمندی | `favoritesList` |
Each tile: `isUserLoggedIn() ? goToDetails(id) : (data.loginNotify = true)` (`:211`).
---
## 5. Guards
### 5.1 `router.beforeEach` — full logic (`src/router/index.ts:250-272`)
```
1. await useCustomAnalytics(to, from, routes) // :253
2. if (!isUserLoggedIn()): // :256
if to.name ∈ {ordersList, transactionsList, profileData,
favoritesList, completeProfile}: // :257
showNotify('ابتدا وارد شوید') // :259
return next({name:'home'}) // :260
else return next() // :261
3. else if (to.name === 'login'
|| to.fullPath === '/'
|| (!memory.state.isLoaded && to.name !== 'home'
&& !to.meta?.skipLoadedCheck)): // :262-265
return next({name:'home'})
4. else if ((myStore.state.sts_verify_step_two === 2 ||
myStore.state.sts_verify_step_two === 1)
&& to.name === 'completeProfile'): // :266
return next({name:'home'})
5. if (to.name === 'singleProduct') nav.setPgaTransition('slideDown')
else nav.setPgaTransition(undefined) // :268-270
6. return next() // :271
```
Notes:
- `noneAuthUrl = ['login']` is declared at `src/router/index.ts:247-249` and **never used**.
- `isUserLoggedIn()` is a pure `localStorage.getItem('appToken')` check with no expiry/JWT validation (`src/Mixins/checkAccess.ts:12-14`).
- **Cold-boot trap:** a logged-in user deep-linking to any route other than `home` before `memory.state.isLoaded` is true gets bounced to `home`, unless the route sets `meta.skipLoadedCheck` (only `addUser` and `customerChoice` do). `isLoaded` becomes true only when **both** `config` and `cats` are set (`src/store/store.ts:160,165`), and `cats` is set by the **Home view** (`src/views/home/index.vue:207`) — so `isLoaded` can only become true after visiting Home.
- `canNavigate` and `getHomeRouteForLoggedInUser` in `src/Mixins/checkAccess.ts:1-11` are **dead code** (never imported).
### 5.2 `router.afterEach` (`src/router/index.ts:273-286`)
Sets `transition.setTransitionFlag(false)` + `setTransitionEnded(false)`, then re-enables the flag after 100 ms; then applies the nav-visibility rule from §4.
### 5.3 KYC / level gating — what actually exists
**There is no KYC level system.** The only verification state is two flat fields on the user object:
| Field | Type | Where read | Behaviour |
|---|---|---|---|
| `sts_verify_step_one` | `number \| string` | `src/store/myDataStore.ts:20` | If `== 3` after OTP verify → refuse login with `'مجوز ورود ندارید'` and **do not** store the token (`src/views/user/LoginRegister/components/Login.vue:167-171`) |
| `sts_verify_step_two` | `number \| string` | `src/store/myDataStore.ts:21` | `∈ {1,2}` → block `/complete_profile` route (`src/router/index.ts:266`); `≠ 1 && ≠ 2` → show "تکمیل پروفایل" CTA on profile (`src/views/user/src/components/profile.vue:96-101`); `=== 2` required to add-to-cart (`src/views/singleProduct.vue:95`) |
`sts_verify_step_two === 2` is therefore the **de-facto "approved" level** and is the only thing that resembles a KYC gate: it gates **add-to-cart** and nothing else.
### 5.4 Role gating (`type` field) — see §8.4 for values
| Condition | Effect | Evidence |
|---|---|---|
| `type == 1` (sales rep) | Show "انتخاب مشتری" button; show "سفارش مشتریان" tile; grid layout switches to 2×col-span-1 | `src/views/user/src/components/Dashboard.vue:37-46,102-129` |
| `type == 1 \|\| hasActiveChild` | Show "add user" FAB on profile | `src/views/user/src/components/profile.vue:22-25` |
| `type !== 1` | Show sales-rep contact card (name + call button) on profile | `src/views/user/src/components/profile.vue:57-80` |
| `type !== 1` | Show single wide "گزارش حساب" tile (col-span-2) | `Dashboard.vue:88-101` |
| `type === 5` | "سفارش ساخت" tile goes **straight** to `makeOrderStatus`; also unlocks confirm/ship buttons on custom orders | `Dashboard.vue:132-141`; `makeOrderStatus.vue:68-75` |
| `type !== 5` | "سفارش ساخت" tile opens a modal with "ثبت سفارش" / "مشاهده سفارش" | `Dashboard.vue:142-165` |
| `type !== 4` (`bankerPermission`) | Wholesaler (`بنکدار`) must **not** see اجرت (wage) — hides wage in cart item and the wage filter | `src/store/myDataStore.ts:86-90`; `src/components/product/cartItem.vue:93-95`; `src/components/search/Filters.vue:24` |
| `type == 4` on customer create | Force `sts = -1` on the payload | `src/views/user/src/components/addUser.vue:152` |
| `mcode` null | `/report` redirects to `home`; "جزئیات حساب" button and balance grid hidden in the report sheet | `src/views/report/index.vue:174-177`; `Dashboard.vue:173,179,199` |
### 5.5 401 / 403
- **401:** only in `myStore.init()` — wipes 3 localStorage keys, clears impersonation, pushes `home`, rethrows (`src/store/myDataStore.ts:136-142`). Because there is no response interceptor, a 401 on **any other** call is swallowed by that call's local `catch`.
- **403:** no handling anywhere.
- **Manual logout:** `Dashboard.vue:781-794` — removes `appToken`, `appCart`, `activeChildToken`, `toggleActiveUser(undefined)`, push `home`, then `window.setInterval(() => window.location.reload(), 1000)` (**an interval, not a timeout** — reloads in a loop until navigation kills it; same pattern at `completeProfile.vue:175-181`, `makeOrder.vue:169-175`, `makeOrderStatus.vue:166-172`). A second, simpler `logOut` exists unused at `src/views/user/index.vue:98-101`.
---
## 6. Views
### 6.1 Full file census under `src/views` (recursive) — 26 files
| # | File | Purpose (one line) |
|---|---|---|
| 1 | `src/views/cart.vue` | Shopping cart: list items, per-item note drawer, total weight, confirm dialog, submit order, success screen. |
| 2 | `src/views/login.vue` | **Dead file — entire contents wrapped in one HTML comment** (`src/views/login.vue:1` opens `<!--`, `:201` closes `-->`). Superseded by `views/user/LoginRegister/`. Not routed. |
| 3 | `src/views/newest.vue` | "Newest products" infinite list (`getProducts` with `{page}` only). |
| 4 | `src/views/singleProduct.vue` | Product detail: gallery, variants, wage/discount, add-to-cart, favorite toggle. |
| 5 | `src/views/home/index.vue` | Home: slider, 9-category grid (custom order), newest products, product carousel, login-nudge dialog after 5 s. |
| 6 | `src/views/home/src/components/NotificationList.vue` | Notification list + unread count + mark-read. **Not mounted** (usage commented at `home/index.vue:5`). |
| 7 | `src/views/report/index.vue` | Financial report: two modes (کلیات / جزئیات) via radio, infinite scroll, per-row detail modal. |
| 8 | `src/views/report/detail.vue` | Single-record ant `Table` with 11 columns from the Pinia `report` store. |
| 9 | `src/views/report/store/report.ts` | Pinia store `report` holding `currentReport: Hesab \| null`. |
| 10 | `src/views/report/components/Accounting.vue` | Card: doc no, Jalali date, gold debit/credit, gold balance. |
| 11 | `src/views/report/components/AccountingDetail.vue` | Detailed accounting card variant (`flag` prop selects list vs. modal mode). |
| 12 | `src/views/search/searchPage.vue` | Product search: search input, category chips, filter panel toggle, infinite product list. |
| 13 | `src/views/search/collection/index.vue` | Collection ("سینی") grid; tapping an item opens `singleProduct` in a bottom drawer. |
| 14 | `src/views/search/collection/components/CollectionCard.vue` | Single tile in that grid. |
| 15 | `src/views/user/index.vue` | Thin wrapper: renders `Dashboard` + the About drawer. |
| 16 | `src/views/user/LoginRegister/index.vue` | Toggles between `Login` and `Register` via a local `showRegister` ref. |
| 17 | `src/views/user/LoginRegister/components/Login.vue` | Phone → OTP login, 120 s resend countdown. |
| 18 | `src/views/user/LoginRegister/components/Register.vue` | B2B jeweler registration + 3-level city picker; success → "await approval" dialog. |
| 19 | `src/views/user/src/completeProfile.vue` | Upload 3 documents (business licence, storefront, national ID) → `/major/complete`. |
| 20 | `src/views/user/src/components/Dashboard.vue` | Main work desk: identity card, gram/rial balances, tiles, gold/currency price list, logout. |
| 21 | `src/views/user/src/components/Orders.vue` | Orders list with 8 status tabs, client-side id search, full-screen detail modal + 4-step progress + invoice. |
| 22 | `src/views/user/src/components/ChildOrders.vue` | Sales-rep view of customers' orders: paged + search, edit item weights, submit / final-submit. |
| 23 | `src/views/user/src/components/transactions.vue` | Financial-document list, detail drawer, download file, FAB → addTransaction. |
| 24 | `src/views/user/src/components/addTransaction.vue` | Full-page 4-tab findoc form (آب شده / حواله / چک / پولی). |
| 25 | `src/views/user/src/components/addFindoc.vue` | **Duplicate** of the above as a bottom drawer; mounted inside `transactions.vue:139` (both FABs exist, overlapping). |
| 26 | `src/views/user/src/components/profile.vue` | Read-only profile card + sales-rep contact + "تکمیل پروفایل" CTA (971 lines, ~85 % commented-out legacy). |
| 27 | `src/views/user/src/components/editUser.vue` | Edit own profile: landline, address, birth/marriage date, 4 image uploaders. |
| 28 | `src/views/user/src/components/addUser.vue` | Sales-rep: create **or** edit a customer (mode from `route.query.edit`). |
| 29 | `src/views/user/src/components/customerChoice.vue` | Sales-rep: customer list, search, impersonate, edit, activate/deactivate. |
| 30 | `src/views/user/src/components/favorits.vue` | Favorites product list with manual pagination. |
| 31 | `src/views/user/src/components/makeOrder.vue` | Custom manufacture order form (image + 5 attributes + description). |
| 32 | `src/views/user/src/components/makeOrderStatus.vue` | Custom-order status list with a 5-stage animated progress rail. |
| 33 | `src/views/user/src/components/about.vue` | Static "About Henza" drawer with background image. |
| 34 | `src/views/user/src/components/report.vue` | **Debug page** — dumps `useScreenOrientation()` values. Unrouted, unmounted. |
(34 files total; 26 `.vue` under `src/views` plus the Pinia store — the numbering above is the file list.)
### 6.2 Requested-flow coverage matrix
| Requested flow | Status | Where |
|---|---|---|
| login / OTP | **PRESENT** | §6.3 |
| register | **PRESENT** | §6.4 |
| buy gold | **ABSENT** — nearest is add-to-cart for a *physical jewelry SKU* | §6.7 |
| sell gold | **ABSENT** | — |
| wallet | **ABSENT** — nearest is the read-only balance strip on Dashboard | §6.5 |
| deposit | **ABSENT** — nearest is submitting a findoc of type چک/حواله/آب‌شده | §6.9 |
| withdraw | **ABSENT** | — |
| transfer | **ABSENT** — nearest is findoc type 2 (`حواله`), which only records a transfer note | §6.9 |
| orders | **PRESENT** | §6.10, §6.11 |
| transactions | **PRESENT** (financial documents) | §6.9 |
| profile / KYC | **PARTIAL** — profile yes; KYC is two flat flags, no levels | §6.12–6.14 |
| products / shop | **PRESENT** | §6.6, §6.7 |
| cart | **PRESENT** | §6.8 |
| addresses | **ABSENT** — address is a single free-text field on the user | §6.13 |
| tickets / support | **ABSENT** — only a static WhatsApp FAB (`index.html:59-61`) | — |
| notifications | **PRESENT but not mounted** | §6.16 |
---
### 6.3 Login + OTP — `src/views/user/LoginRegister/components/Login.vue`
**API**
| Step | Call | Payload | Evidence |
|---|---|---|---|
| 1 | `POST /web/auth` | the whole ant-design `values` object → `{username}` | `:137` |
| 2 | `POST /user-verfiy` | `{username: formModel.username, password: <otp string>, token_mobile: unref(token.fireBaseToken)}` | `:157-165` |
**Fields & validation**
| Field | Component | Rules | Exact message | Evidence |
|---|---|---|---|---|
| `username` (تلفن همراه) | `<Input type="number">` | `required('تلفن همراه')`, `mobile('تلفن همراه')` | required → `' لطفا تلفن همراه را وارد کنید '`; pattern fail → `' مقدار تلفن همراه نا معتبر است '` | `:63-66`; messages `src/Mixins/validator/index.ts:41,1-3` |
| OTP | `<otp-input :num-inputs="4" inputType="tel">` | none (auto-submits on completion) | — | `:45-52` |
Mobile regex: `/^(\+98|0098|98|0)?9\d{9}$/g` (`src/Mixins/validator/index.ts:6`). **Note the `g` flag with `.test`-style reuse is a known stateful-lastIndex hazard.**
**Timer / resend:** `timer = 120` on successful step 1 (`:140`); recursive `setTimeout` decrement (`:122-131`). OTP inputs render only while `step==2 && timer>0` (`:46`). At `timer===0` the text switches to `برای دریافت رمز عبور مجدد تلاش کنید` (`:54-56`); while running it reads `امکان دریافت مجدد رمز در {counterZeroShow(timer)} ثانیه دیگر` with zero-padding below 10 (`:57-60`, `:187-189`).
**Button:** label `body.step === 1 ? 'دریافت رمز عبور' : 'دریافت مجدد رمز عبور'` (`:192-194`); `:loading="body.loading"`; `:disabled="timer > 0 && body.step === 2"` (`:68`).
**Errors / success**
| Condition | Behaviour | Evidence |
|---|---|---|
| step-1 response 404 | red `NotifyVant` with default text `اطلاعات کاربری در سیستم وجود ندارد.` | `:149-151`, `:112` |
| `data.sts_verify_step_one == 3` | notify `مجوز ورود ندارید`, **return without storing the token** | `:167-171` |
| step-2 any error with a status | notify `خطا در ورود` | `:179-182` |
| success | `localStorage.setItem('appToken', data.api_token)` → `await myStore.init()` → `router.push({name:'home'})` → notify `با موفقیت وارد شدید` | `:172-176` |
Notify colour is hard-coded red (`background-color: #DC2626`) regardless of success/failure (`:3`).
**Web OTP API:** `useOtpToken()` registers a `navigator.credentials.get({otp:{transport:['sms']}})` listener that pushes the code into `token.setToken` (`src/store/loginToken.ts:34-52`) — but it is **commented out** in `src/App.vue:100`, and the `watch(token.token, …)` auto-submit lives only in the dead `src/views/login.vue:160-166`. **SMS auto-fill is therefore inactive.**
Back arrow → `userPage::hasSubRoute` (`:11-12`). "ثبت نام کنید." emits `register(true)` (`:78`).
### 6.4 Register — `src/views/user/LoginRegister/components/Register.vue`
**API:** `POST /major/register` with the form `values` (`:332`). Cities loaded on mount via `POST /cities/list` (`:264-266`, `:296-307`).
| Field | Component | Rules | Evidence |
|---|---|---|---|
| `fname` (نام) | Input | `required('نام')` | `:29-31` |
| `lname` (نام خانوادگی) | Input | `required('نام خانوادگی')` | `:32-34` |
| `mobile` (شماره موبایل) | Input type=number | `mobileValidator(...)`, `required(...)` | `:35-38` |
| `store_name` (نام فروشگاه) | Input | `required('نام فروشگاه')` | `:39-41` |
| `phone` (تلفن فروشگاه) | Input type=number | `required('تلفن فروشگاه')` only — **no `phone()` pattern applied** | `:42-45` |
| `address` (آدرس پستی) | Textarea | `required('آدرس')` | `:46-48` |
| `city` (شهر) | **plain Input** | `required('شهر')` | `:49-66` |
**City cascader is dead UI:** the 3-level country→province→city `WorkActionSheet` is fully implemented (`:80-134`, `recursiveFunction` `:272-294`, handlers `:309-327`) and the data is fetched, but the trigger button is **commented out** (`:58-64`) — the field degrades to a free-text `city`. `onSelectCities` writes `formModel.city_id`, which is never submitted.
**Submit:** button `ثبت اطلاعات`, `html-type="submit"`, no explicit disabled/loading (form is wrapped in `<Spinner :spinning="profileData.loading" tip="در حال ثبت اطلاعات">`, `:26`).
**Success:** `registeredInfo = true` → one-button dialog (`DialogVantOneButton`, `:closeOnClickOverlay="false"`) reading `ثبت نام شما با موفقیت انجام شد. منتظر تایید اطلاعات ثبت نامی شما توسط کارشناسان بمانید.`; button `متوجه شدم` → `router.push({name:'home'})` (`:135-152`).
**Failure:** `console.log(e)` only — **no user-visible error** (`:340-342`).
### 6.5 Dashboard ("میز کار") — `src/views/user/src/components/Dashboard.vue`
The wallet-equivalent surface. Rendered by route `/user` via `src/views/user/index.vue:27`.
**Identity card** (logged in, `:24-56`): name (`:27`), "انتخاب مشتری" / "خروج از حساب مشتری" toggle button shown when `type==1 || hasActiveChild` with `type` switching `primary`↔`danger` (`:37-46`), mobile row (`:51-54`).
**Logged-out card** (`:57-64`): `برای استفاده از امکانات نرم افزار ابتدا وارد شوید` + `ورود/ثبت نام` button → `/login`.
**Balance strip** (`:66-83`) — the closest thing to a wallet:
| Label | Value | Computation | Evidence |
|---|---|---|---|
| `بدهکار گرمی` when `remain > 0`, else `بستانکار گرمی` | `Math.abs(remain)` + ` گرم` | `myStore.state.remain` | `:69-70`, `:411-415`, `:402-404` |
| `فروش گرمی` (only `type == 1`) | `grams_sell` + ` گرم` | `myStore.state.grams_sell` | `:72-75`, `:416-418` |
| `بدهی ریالی` | `$filters.priceUnit(rial_remain)` → thousands-separated + ` تومان ` | `myStore.state.rial_remain` | `:77-78`, `:419-421`; filter `src/Mixins/filters/index.ts:11-16` |
> **Unit inconsistency:** the field is named `rial_remain` but `priceUnit` appends **تومان** (`src/Mixins/filters/index.ts:13`), while the price list appends **ریال** (`Dashboard.vue:258`).
**Account-report action sheet** (`handleShowReport`, `:545-560`): if `myStore.hasMCode()` → `GET /tahesab/me`, store `data.result` in `mandeh`, then open a `van-action-sheet`.
- No mcode → empty illustration `gozareshEmpty` (`:173`).
- Has mcode → 2 boxes (`:179-198`):
- "ته حساب طلایی(گرم)" renders `usePrice(mandeh[1]?.mandeh, true, '')`, coloured by `mandeh[0]?.mandeh > 0 ? text-green : text-red`
- "ته حساب ریالی(ریال)" renders `useThreeDecimal(mandeh[0]?.mandeh)`, coloured by `mandeh[1]?.mandeh > 0`
- **The index/colour pairing is crossed in both boxes** (`:182,184` and `:190,192`) — gram box shows index 1 formatted as money, rial box shows index 0 formatted as 3-decimals.
- "جزئیات حساب" button → `router.push({name:'report'})` (`:199-204`, `:449-451`).
**Tiles:** see §4 (Dashboard grid). Every tile: `isUserLoggedIn() ? action : (data.loginNotify = true)`.
Note `:119` has a bug: `@click="isUserLoggedIn()?openChildOrders:data.loginNotify = true"` — `openChildOrders` is referenced, **not invoked**, so the child-orders tile never navigates.
**Dialogs** (all `DialogVant`, `:timeOut="-1"`):
| Trigger | Title | Buttons | Evidence |
|---|---|---|---|
| activate/deactivate customer | `کاربر غیر فعال شود؟` / `ّکاربر فعال شود؟` (note the stray `ّ`) | بله → `setActive`; بی خیال | `:286-300`, `:584-598` |
| logout icon | `آیا می خواهید از برنامه خارج شوید؟` | بله → `logOut`; بی خیال | `:302-316` |
| any gated tile while logged out | `برای دسترسی به تمامی امکانات نرم افزار هنزاگلد، ابتدا گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید.` | ورود/ثبت نام → `/login`; بیخیال | `:317-334` |
**NotifyVant toasts:** `درحال دریافت اطلاعات` orange ×2, `قیمت ها با موفقیت به روز رسانی شد` green, `خطا در به روز رسانی` red — all 4000 ms (`:269-284`).
Analytics: `useCustomAnalyticsComponent('داشبورد')` (`:688`).
### 6.6 Home — `src/views/home/index.vue`
`POST /front` on mount (`:181-223`). Response is deep-cloned via vant's `deepClone` (`:185`). Categories are re-ordered against a hard-coded layout array `[37, 45, 31, 32, 42, 26, 36, 35, 34]`, matched ids first (in that order), remainder appended (`:189-204`). `memory.setCats(data.cats)` (`:207`) — this is what flips `isLoaded` true and unblocks the router's `skipLoadedCheck` gate (§5.1).
After 5 s, if `!isUserLoggedIn()`, show the login-nudge dialog and call `memory.setFirstTimeLoaded()` (`:210-216`). The `firstLoadFront` "only once" guard is written but **commented out of the condition** (`:212`), so the nudge fires on every Home visit.
Sections: slider (`memory.state.config?.main_slider`, height = `min(width,768)/2.2125` px, `:11`, `:245`), 3-col category grid (`:14-25`), "محصولات جدید" horizontal product cards + "مشاهده همه" → `newestProduct` (`:27-38`), then a horizontally-scrolling vertical-card rail ending in a `view-all` tile → `/search` (`:40-44`).
Whole page is behind `v-if="flag"` and a `Spinner` with tip `درحال دریافت اطلاعات` (`:2`, `:10`).
### 6.7 Single product — `src/views/singleProduct.vue`
`GET /product/${id}` — called **twice** in the same function, once for the route param `id` and once for the `props.id` drawer form; both branches run identical 18-line variant-selection blocks (`:280-325`). Duplicate fetch when both are set.
**Variant selection:** default = the variant with the **lowest `percentage_pay`** (`:286-296`, then overwritten by `getSelectedVariant(unref(variants))` at `:296` which does the same thing — `src/Mixins/product/index.ts:54-67`). Empty variants fall back to `{grams:'0', id:0, title:'بدون تنوع', percentage_pay:0}` (`:287`).
**Displayed calculations:**
| Display | Formula | Evidence |
|---|---|---|
| Wage % | `getWage(variant) = myStore.state.percent + variant.percentage_pay` | `src/Mixins/product/index.ts:69-75`; rendered `{{ wage % 1 === 0 ? wage : wage?.toFixed(2) }}% اجرت` (`singleProduct.vue:66`) |
| Discounted wage % | `getHasDiscountWage = (variant.percentage_pay - promotion.discount_percent) + myStore.state.percent`, **only if `myStore.state.percent > 0`** | `src/Mixins/product/index.ts:77-83` |
| Discount badge | `-%{{ promotion.discount_percent }}` | `singleProduct.vue:60` |
| Grams | `{{ selectedVariant.grams }}` | `:73` |
**Promotion engine** (`src/Mixins/product/index.ts:11-52`):
- `checkIgnoreList(product)` — an `ignoreList` entry suppresses all promotions when `type==1 && item_id==product.id`, `type==2 && item_id==product.category.collection_id_id`, `type==3 && item_id==myStore.state.id`, `type==4 && item_id==product.brand.id` (`:11-26`).
- `getCandidPromotions` matches promotions by the same 4 types; **`type==3` (customer-type discount) is hard-disabled** with `if (promotion.type == 3) return false` and the Persian comment `--- موقتا غیرفعال: تخفیف نوع مشتری (مربوط به ورود/ثبت‌نام) ---` (`:31-32`).
- `getPromotion` picks the candidate with the maximum `discount_percent` (`:47-52`).
**Add-to-cart button** (`:92-99`): two variants exist; the live one is
`@click="isUserLoggedIn() ? (myStore.state.sts_verify_step_two === 2 ? addToCart() : loginStepTwoNotify = true) : loginNotify = true"` (`:95`) — **this is the only verification gate on a transactional action in the app.** Rendered only when `!cart.isInCart(storeProduct)`; otherwise an "already in cart" button (`:101-103`).
Cart payload shape: `{vid: selectedVariant.id, note: null, pid: product.id, product, qty: 1}` (`:259-266`).
Favorite toggle: `toggleFavorite(props.id)` — optimistic local flip then `POST /add-to-fav` (`:108-111`; `src/Mixins/favorite.ts:5-13`).
The view hijacks the back button: `history.pushState(null, location.href)` + `window.onpopstate = () => history.go(1)` (`:376-379`) — a global side effect executed at setup time.
### 6.8 Cart — `src/views/cart.vue`
**Storage:** `useStorage('appCart', [])` from `@vueuse/core` with a JSON serializer — persisted in `localStorage` under key `appCart` (`src/store/cartStore.ts:19-27`).
**Hydration:** `onMounted → cart.init()` re-fetches all products by id via `POST /get-products {products_id}` and rebuilds each line, **silently dropping** any line whose product or variant no longer exists (`src/store/cartStore.ts:36-64`).
**Per-item controls** (`src/components/product/cartItem.vue`): `+` increments (`:29`, `:69-72`); when `qty === 1` the minus becomes a **trash** icon that removes the line (`:32-35`); otherwise minus decrements (`:37-38`, `:73-76`). Displays: وزن حدودی (`selectedVariant.grams` گرم, `:10`), اجرت (`wage?.toFixed(2)`% with line-through when a discounted wage exists, `:13-19`), اجرت ریالی (`$filters.priceUnit(selectedVariant.rial_pay)`, only when logged in, `:21-24`). Wage block is gated by `showDiscountWage = myStore.bankerPermission && hasDiscountWage && isUserLoggedIn()` (`:93-95`).
**Note drawer** (`src/views/cart.vue:46-59`): bottom `Drawer` height `280px`, `Textarea :rows="5"`, submit `ثبت` → `cart.updateFeild(value.note, index, row, 'note')` (`:151-156`).
**Total:** `cart.total()` = `Σ(variant.grams × qty)` `.toFixed(2)` — **weight only, no money** (`src/store/cartStore.ts:118-122`; rendered `جمع وزن : {{ cart.total() }} گرم`, `src/views/cart.vue:99-101`).
**Bottom bar** visible only when `items.length > 0 && step === 1` (`:98`). Button `ثبت نهایی سفارش` sets `cartData.notify = true`; the `:disabled="cartData.notify"` variant is commented out at `:102` in favour of an always-enabled button at `:103`.
**Confirm dialog:** `آیا مطمئن به ثبت سفارش هستید ؟` with بله → `submitOrder`, بی خیال → dismiss (`:61-75`).
**Submit** (`:157-194`) → `POST /set-order`. Per-item payload:
| Key | Source |
|---|---|
| `discount` | `getPromotion(product)?.discount_percent \|\| 0` |
| `id` | `null` |
| `note` | `cartItem.note` |
| `percent_pay` | `getWage(cart.variant(cartItem))` |
| `product` | `cartItem.pid` |
| `promotion_id` | `promotion?.id \|\| null` |
| `promotion_item_id` | `cartItem.pid` |
| `qty` | `cartItem.qty` |
| `rash_weight` | `cart.variant(cartItem).grams` |
| `rial_pay` | `0` |
| `variant` | `cartItem.vid` |
| `wage_weight` | **hard-coded `5.7856000000000005`** with the comment `// how to get?!` (`:181`) |
| `weight` | **hard-coded `6.303744000000001`** with the comment `// how to get?!` (`:182`) |
Top-level: `final_submit: false`; `from_parent: unref(myStore.hasActiveChild)` with the comment "true only when a rep orders on a customer's account, otherwise the SMS wouldn't reach the rep" (`:162-164`); `id: null`.
**Success:** `cartData.orderID = id`, `step = 2`, `cart.clear()` (`:186-188`). Step-2 screen: `اطلاعات سفارش با موفقیت ثبت شد`, `شماره سفارش {{orderID}}`, button `لیست سفارشات` → `/orders` then resets `step = 1` (`:31-41`).
**Failure:** `console.log(e)` only (`:189-191`).
**Empty state:** `<empty :image="cartEmpty"/>` when `items.length === 0` (`:24`); `empty.vue` always renders the description `موردی یافت نشد` (`src/components/empty.vue:4`).
### 6.9 Transactions / financial documents
**List — `src/views/user/src/components/transactions.vue`**
`POST /get-my-findoc` on mount → `{rows}` (`:200-215`). **No pagination params, no filters, no search, no tabs.**
Row (`:19-58`): icon `vuesax-linear:add-square` when `type != 9` else `vuesax-linear:minus-square` (`:245-247`); background `#F0FDF4`/foreground `#22C55E` when `type != 9`, else `#FEF2F2`/`#F44336` (`:232-238`). `hasFactor(item) = item.type != 9` (`:249-251`). Shows `title`, `type_str` chip, `{{value}} گرم`, `uaj` (update date), `sts_str`, and — only when `hasFactor` — a `دریافت فایل` button opening `https://henzagold.com/oapi/api/report/doc?doc_id=${id}` with `e.stopPropagation()` (`:43-49`, `:223-226`).
Detail drawer (`:65-138`, height `450px`): نوع سند (`type_str`), تاریخ ثبت سند (`caj`), تاریخ ویرایش سند (`uaj`), حواله به (`to`), مبلغ (`$filters.priceUnit(price)` or `- -`), توضیحات (`note` or `- -`).
Empty: `<empty :image="transactionEmpty"/>` (`:60`). A `testBind()` helper marks the first 4 rows for a CSS transition class but **returns `undefined`**, so `test.value` becomes `undefined` after assignment (`:177-198`, `:205`).
Two "add" affordances overlap: the FAB in this file → `/addTransaction` (`:141-143`) and the FAB inside the embedded `<AddFindoc @change="getFindocs"/>` → opens the drawer (`:139`; `addFindoc.vue:5-7`).
**Create — `addTransaction.vue` (page) and `addFindoc.vue` (drawer), functionally identical**
Tabs (`addTransaction.vue:184-189`, `addFindoc.vue:192-197`) — these are the transaction **type** enum:
| `type` | Label |
|---|---|
| 1 | `آب شده` |
| 2 | `حواله` |
| 3 | `چک` |
| 4 | `پولی` |
Default `type: 1` (`addTransaction.vue:170`).
| Shown for type | Field | Component | Rule | Exact message |
|---|---|---|---|---|
| 1, 2 | `w` (وزن) | `InputNumber :min="0" size="large" class="hide-counter" type="number"` | `required('وزن')` | `' لطفا وزن را وارد کنید '` |
| 1 | `ayar` (عیار) | `InputNumber :min="0" type="number"` | `required('عیار')` | `' لطفا عیار را وارد کنید '` |
| 1 | `lab` (آزمایشگاه) | Input | `required('آزمایشگاه')` | `' لطفا آزمایشگاه را وارد کنید '` |
| 1 | `ang` (انگ) | Input type=number | **none** | — |
| 2 | `to` (حواله به) | `RadioGroup` over `myStore.state.to`, **`:value="index"` (array index, not the string)** | none | — |
| 3 | `price` (مبلغ (ریال)) | `InputNumber :min="0"` | `required('مبلغ')` | `' لطفا مبلغ را وارد کنید '` |
| 3 | `no` (شماره چک) | `InputNumber type="number"` | `required('شماره چک')` | `' لطفا شماره چک را وارد کنید '` |
| 3 | `proceed_at` (تاریخ) | `InputDatePicker` (Jalali) | `required('تاریخ')` | `' لطفا تاریخ را وارد کنید '` |
| 3 | `account_name` (نام صاحب حساب) | Input | `required('نام صاحب حساب')` | `' لطفا نام صاحب حساب را وارد کنید '` |
| 3 | `bank_name` (نام بانک) | Input | `required('نام بانک')` | `' لطفا نام بانک را وارد کنید '` |
| 4 | — | static text | — | `مشتری عزیز لطفا جهت پولی کردن با کارشناس فروش خود تماس حاصل نمایید.` |
Evidence: `addTransaction.vue:28-82` / `addFindoc.vue:32-86`; validator messages from `src/Mixins/validator/index.ts:41`.
**No input masks, no thousand separators, no Persian→English digit conversion** on any of these numeric fields — they are raw `InputNumber`s.
**Submit button** hidden entirely when `type === 4` (`addTransaction.vue:85`, `addFindoc.vue:91`). Label `ثبت اطلاعات`.
**Bug (page version):** the button sits **outside** the `<Form>` element (`addTransaction.vue:84-94` closes the form at `:97`) and carries no `html-type="submit"`, so `@finish="setFindoc"` never fires from a click. The drawer version has the same structural problem (button at `:91-100`, form closed at `:90`) but at least declares `html-type="submit"`.
**Success:** `FindocData.notify = true` → green NotifyVant `اطلاعات با موفقیت به ثبت رسید` (4000 ms), close drawer, `findocForm.value.resetFields()`, `emit('change')` (which `transactions.vue` maps to `getFindocs`). **Failure:** `console.log(e)` only.
### 6.10 Orders — `src/views/user/src/components/Orders.vue`
`POST /get-orders {sts}`; initial call `getOrders(-1)` on mount (`:287-299`, `:343`). Tab change passes the tab key directly as `sts` (`:24`).
**Status tabs / order status enum** (`:259-270`):
| key (`sts`) | Title |
|---|---|
| -1 | همگی |
| 0 | لغو شده |
| 1 | ثبت سفارش |
| 2 | تایید مدیر سیستم |
| 3 | درحال تامین |
| 4 | درحال آماده سازی |
| 5 | تحویل مدیر فروش |
| 6 | تحویل مشتری |
**Status colours** (`setColor`, `:306-326`, mirrored in `src/components/product/order.vue:67-86`):
| sts | font | background |
|---|---|---|
| 1 | `#3B82F6` | `#EFF6FF` |
| 3 | `#EAB308` | `#FEFCE8` |
| 4 | `#F97316` | `#FFF7ED` |
| 5 | `#06B6D4` | `#ECFEFF` |
| 6 | `#22C55E` | `#F0FDF4` |
| 0, 2 | **unset** (transparent chip) | — |
**Progress rail** — 4 steps `['ثبت سفارش','آماده سازی','تحویل مشتری','لغو شده']` (`:244`); `setCurrentStep()` maps `sts ∈ {1,2} → 0`, `{3,4} → 1`, `{5,6} → 2`, `0 → 3` (`:246-257`). Default `current = 1` before any order is opened (`:245`).
**Search:** client-side only, exact id match — `orderData.items.filter(x => x.id == value)` (`:279-285`). No API param.
**Pagination:** none — the endpoint returns everything.
**Detail modal** (`:50-203`, class `${prefix}-custom-modal` = full-screen, `:mask="false" :footer="false" :closable="false"`): شماره سفارش `ORD-{{id}}`, وضعیت chip, `items.length` آیتم, `w?.toFixed(2)` گرم, `caj` date. Item cards via `OrderItem` showing تعداد/وزن/اجرت(درصد)/توضیحات (`src/components/product/orderItem.vue:9-34`).
**Invoice:** button `دریافت فاکتور` → `window.open('https://henzagold.com/oapi/api/report/order?order_id=' + id, '_bank').focus()` (`:63-66`, `:333-335`).
**Empty:** `<empty :image="ordersEmpty"/>` when the filtered list is empty (`:44`).
Analytics: `useCustomAnalyticsComponent('لیست-سفارشات')` (`:339`).
### 6.11 Child orders (sales rep) — `src/views/user/src/components/ChildOrders.vue`
`POST /order/child {page, search}`, `page` post-incremented per fetch; `fetchFlag` false once a page returns 0 rows; `flag` true when the list is empty (`:329-341`). Search resets `page = 1`, clears the list, refetches (`:371-377`).
**Edit flow:** `toEditOrder` snapshots `itemsWeight = items.map(i => ({w: +(i.w - i.wage).toFixed(2), wage: i.wage}))` — i.e. it edits the **raw** (pre-wage) weight (`:344-351`). `totalWeight = Σ(w + wage)` `.toFixed(2)` (`:424-426`).
**Submit** (`:385-407`): `PUT /order/child/${id}` with `items` rebuilt as `{...originalItem, w: item.w + item.wage, rash_weight: item.w}`, plus `final_submit` and `from_parent: true`.
**Confirm text** depends on the mode (`:380-383`): `آیا مطمئن به ثبت نهایی هستید ؟` when `final_submit`, else `آیا مطمئن به ثبت تغییرات سفارش خود هستید ؟`. `toFinalSubmit()` / `toSubmit()` set the flag then open the dialog (`:409-417`).
Analytics: `useCustomAnalyticsComponent('سفارش مشتریان')` (`:429`).
### 6.12 Profile — `src/views/user/src/components/profile.vue`
**Read-only. Makes no API calls** — every `axios` line in the file is commented out (`:574`, `:796`, `:877`). All data from `myStore.state` computeds (script-setup block starting `:110`).
Shows: avatar (`myStore.state.image?.image`, falls back to the Henza logo, `:44-49`), sales-rep card with name + a `تماس` button calling `window.open('tel:' + reagent.mobile)` — rendered only when `type !== 1` (`:57-80`), then نام و نام خانوادگی / تلفن همراه / آدرس (`:81-94`).
**"تکمیل پروفایل" CTA** → `/complete_profile`, shown only when `sts_verify_step_two !== 2 && sts_verify_step_two !== 1` (`:96-101`).
**"add user" FAB** → `/addUser`, shown when `type === 1 || hasActiveChild === true` (`:22-25`). (Note `hasActiveChild` is a `Ref`, so `=== true` is always false — the `.value` is missing here, unlike `Dashboard.vue:37` which uses `.value` correctly.)
**Edit pencil** → `/editUser` (`:9`).
Analytics: `useCustomAnalyticsComponent('پروفایل')`.
### 6.13 Edit profile — `src/views/user/src/components/editUser.vue`
`POST /update-profile` with `values` + `values.image_id = userFormModel.image_id` (`:285-291`).
| Field | Component | Rule | Message |
|---|---|---|---|
| profile image | `SingleImageUploader` (only when `type !== 1`) | none | — |
| `phone` (تلفن ثابت) | Input type=number | `phoneValidator('تلفن ثابت')` → `/^((\+98\|0098\|98\|0)\d1)?\d{8}$/g` | `' مقدار تلفن ثابت نا معتبر است '` |
| `address` (آدرس) | `Textarea show-count :maxlength="100"` | none | — |
| `birth_date` (تاریخ تولد) | `InputDatePicker` | none | — |
| `marriage_date` (تاریخ ازدواج) | `InputDatePicker` | none | — |
| 3 more uploaders (جواز کسب / سردر مغازه / کارت ملی) | `SingleImageUploader size="95px"` | none | **their `done` handlers are empty stubs — the ids are discarded** |
Evidence: template `:20-77`; handlers `editUser.vue` script `:66-79` (relative to the 200-offset excerpt, i.e. absolute ≈ `:265-278`); note that two of the three uploaders share the same `v-model:fileList="shopFrontFileList"` (`:65`, `:69`).
**Success:** `myStore.setValue(data)` then `showNotify({message:'ویرایش با موفقیت انجام شد', color:'#ffffff', background:'#22C55E', duration:4000})` then `back()` → `/profile`.
Analytics: `useCustomAnalyticsComponent('ویرایش کاربر')`.
### 6.14 Complete profile (document upload) — `src/views/user/src/completeProfile.vue`
Three `SingleImageUploader`s bound to `business_license_image_id` (بارگذاری عکس جواز کسب), `shop_image_id` (بارگذاری عکس سردر مغازه), `ncode_image_id` (بارگذاری عکس کارت ملی) (`:44-59`).
**Client-side rule** (`:191-198`): only if **all three** ids are non-null → `POST /major/complete`; otherwise `showNotify('لطفا تمامی عکس های خواسته شده را آپلود کنید')`. The `FormItem`s carry **no `rules`** — this is the only validation.
**Success:** dialog (`closeOnClickOverlay:false`) `تکمیل اطلاعات شما با موفقیت انجام شد. منتظر تایید مرحله دوم اطلاعات ثبت نامی شما توسط کارشناسان بمانید.`, button `متوجه شدم` → `refresh()` = push `home` + the reload-interval pattern (`:94-111`, `:175-181`).
### 6.15 Add / edit customer — `src/views/user/src/components/addUser.vue`
**Mode:** `isEditMode = !!route.query.edit` (`:117`). Prefill order on mount: `sessionStorage.editCustomer` (set by `customerChoice.editCustomer`) → else `history.state.customer` (`:242-265`).
Fields & `userFormRules` (`:227-235`): `fname` نام, `lname` نام خانوادگی, `mobile` شماره تلفن همراه, `birth_date` تاریخ تولد (`InputDatePicker`), `type` نوع مشتری (select fed by endpoint #46), `store_name` نام مغازه, `phone` تلفن ثابت, `pasazh_name` نام پاساژ, `new_city` شهر (free text, mirrored into `city` by `syncCityName`, `:122-124`), `zone` منطقه (action-sheet picker over 22 hard-coded Tehran districts `منطقه 1` … `منطقه 22`, `:~170-330`), `address` آدرس. All required except `type` and `zone`; messages are the shared `' لطفا <field> را وارد کنید '`.
**Submit transform** (`:149-193`): `type === '4' ? sts = -1 : delete sts`; `reagent = unref(mobile)`; `city = new_city`; `city_id = null`. Then `POST /customer/update {...model, id: route.query.edit}` or `POST /customer/create model`.
**Toasts:** success `ویرایش کاربر با موفقیت انجام شد` / `افزودن کاربر با موفقیت انجام شد` (green `#22C55E`, 4000 ms); failure `ویرایش کاربر با خطا مواجه شد` / `شماره وارد شده تکراری می باشد` (red `#b10f0f`). The 422/duplicate-mobile branch at `:182` has an **empty body**, so the generic message fires either way.
**Back:** `route.query.from === 'customerChoice'` → `/customerChoice`, else `/profile` (`:234-240`).
### 6.16 Notifications — `src/views/home/src/components/NotificationList.vue` (**not mounted**)
`POST get-notify {page}` with post-increment and `fetchFlag` stop-on-empty (`:99-111`); `GET /notify/count` → unread badge (`:115-125`); `PUT notify/read/${id}` on open, then refresh count and set `notifications[index].read = true` (`:128-144`). Bootstraps both list and count in parallel `onBeforeMount` (`:145-150`). Dead because its tag in Home is commented out (`src/views/home/index.vue:5`).
### 6.17 Custom manufacture orders
**Create — `makeOrder.vue`**
| Field | Component | Rule | Exact message |
|---|---|---|---|
| `file_id` | `SingleImageUploader` (`بارگذاری عکس سفارش`) | `{required:true}` | `لطفا تصویر سفارش را آپلود کنید` |
| `color` رنگ محصول | Input | required | `لطفا رنگ محصول را وارد کنید` |
| `size` سایز محصول | Input | required | `لطفا سایز محصول را وارد کنید` |
| `color_mina` رنگ مینا | Input | required | `لطفا رنگ مینا را وارد کنید` |
| `color_stone` رنگ سنگ | Input | required | `لطفا رنگ سنگ را وارد کنید` |
| `sum` تعداد | Input type=number | required | `لطفا تعداد را وارد کنید` |
| `description` توضیحات | `Textarea :rows="3"` | none | — |
Evidence `:31-61`. Submit → `POST /create-custom-order` (`:221`), preceded by a redundant runtime guard `if (!formOrder.file_id) showNotify({type:'warning', message:'لطفا تصویر سفارش را آپلود کنید'})` (`:206-209`). Success → `showNotify({type:'success', message:'سفارش با موفقیت ثبت شد'})` + push `home` (`:224-225`); failure → `showNotify({type:'error', message:'خطا در ثبت سفارش'})` (`:229`). The local `const loading = ref(false)` is redeclared **inside** `submitOrder` (`:201`), shadowing the outer one, so the spinner never reflects this request.
A leftover `updateProfile` function posting `/major/complete` also sits in this file, unreferenced (`:181-198`).
**Status — `makeOrderStatus.vue`**
`POST /me-data` then `GET /get-custom-orders` on mount (`:252-255`). Stage mapping from the server's Persian `sts_str_fa` (`:147-164`):
| `sts_str_fa` | stage |
|---|---|
| `ثبت سفارش` | 1 |
| `تایید ادمین` | 2 |
| `تایید کارگاه` | 3 |
| `ساخته شده آماده ارسال` | 4 |
| `تحویل شده` | 5 |
Rail labels: ثبت سفارش → تایید ادمین → تایید کارگاه → ارسال → تحویل شده, with a `gradient-bg` animated connector on the current stage and grey/secondary colouring by comparison (`:28-56`). Card fields: رنگ محصول, سایز محصول, رنگ مینا, رنگ سنگ, تعداد, وضعیت (`sts_str_fa`), تاریخ ثبت / ساعت ثبت split from `time_creat_fa` on `-` (`:59-66`), plus `file.image` thumbnail (`:77`).
Actions, **only when `myStore.state.type === 5`** (`:68-75`): `تایید سفارش` → `confirmOrder(id)` → `POST /update-custom-order/{id} {sts:2}` then locally bump the rail to 3 and add the id to `confirmedOrders`; once confirmed the button becomes `ارسال سفارش` → `completeOrder(id)` → **the same `{sts:2}` payload** then removes the row from the list (`:209-250`). `confirmedOrders` is a plain in-memory `Set`, so the confirm/ship distinction is lost on reload.
### 6.18 Customer choice — `src/views/user/src/components/customerChoice.vue`
`POST /my-child` on mount (`:181-192`, `:208-210`). A `watch` on `viewDrawer` also refetches (`:195-197`) — legacy from when this was a drawer.
Card shows image (or `Vector.svg` fallback), name, mobile, phone, `$filters.jdate(birth_date)` or `بدون تاریخ`, address (`:24-56`).
Actions: tap the card → `myStore.toggleActiveUser(customer)` (impersonate) → push `/user` (`:156-168`); pencil → `sessionStorage.editCustomer = JSON.stringify(customer)` + push `addUser?edit=<id>&from=customerChoice` (`:148-154`); `user-tick`/`user-remove` → confirm dialog then `POST /customer/toggle-active {id}` + refetch (`:62-67`, `:141-147`, `:198-204`).
Search: client-side `customerList.filter(x => x.name.includes(value))` (`:169-179`).
Empty: `<empty :image="customerEmpty"/>` (`:16`).
### 6.19 Search — `src/views/search/searchPage.vue`
Query object lives in the module-level `filtersStore` (`src/store/filtersStore.ts:15`), shared across views. On mount (only when the current route is `search`): reset `productList`, `text = undefined`, `ciny_id = undefined`, fetch (`:194-204`).
Pipeline: every mutation funnels through `toggleFilter()` → close panel, clear `productList`, `getProducts(query, productsData, loadingPage, false)` (`:120-124`).
- `search(value)` — only refetches if `value !== query.text` (`:136-141`)
- `setCats(id)` — `cats = [id]` or `undefined` (`:142-146`)
- `getByQuery()` — resets `page = 0` then refetches (`:132-135`)
- `setDefaultQuery()` — preserves `text`, resets everything else to `defaultQuery` (`:126-131`)
Filter chip label: `${filterKeys.length} فیلتر` when any filter is active, else `اعمال فیلتر` (`:17-22`).
Pagination (`src/Mixins/getProducts.ts:14-38`): `totalPages = Math.floor(count/limit) - 1`; `getProducts(..., addPage=true)` only advances when `totalPages >= query.page`, then `query.page++`; rows are **concatenated**; `flag = (productList.length < 1)` drives the empty state. Collections are refetched alongside: `GET /ciny/{cats[0]}` when a category is selected, else `POST /front` → `data.ciny`, plus a 6-item random sample (`:39-60`).
**Filter panel — `src/components/search/Filters.vue`**
| Control | Bound query key | Source of options / bounds | Gating |
|---|---|---|---|
| Colour checkboxes | `colors: number[]` | `myStore.state.filterColor` | — |
| Brand checkboxes | `brands: number[]` | `myStore.state.filterBrand` | — |
| Wage range slider (`:step="0.01"` range) | `limit_percent: [min,max]` | `myStore.state.filterPercentPay` | `v-if="isUserLoggedIn() && myStore.bankerPermission"` (`:24`) |
| Gram range slider (`:step="0.01"` range) | `limit_gram: [min,max]` | `myStore.state.filterGram` | — |
| Discount switch | `has_discount: boolean` | — | — |
`filterKeys` is recomputed on apply: a range counts as "active" only when **both** endpoints differ from the store bounds (`:111-117`). If `filterKeys.length > 0` → emit `update:filterKeys` + `change {resetFilters:false}`; else → emit `[]` + `change {resetFilters:true}` (`:119-125`).
### 6.20 Report — `src/views/report/index.vue` + `detail.vue`
Guard: `onBeforeMount` → `if (!myStore.hasMCode()) router.push({name:'home'})` (`index.vue:174-177`).
Mode radio (`:14-17`): `1` = کلیات حساب → `hesabDetailNew()` = `GET /fin_tahesab/table`, result `data.result.reverse()`; `2` = جزئیات حساب → `accountingDetailList(currentPage ?? 1)` = `GET /fin_tahesab/my_detail/{offset}`, result `data.result` (`:182-210`). Switching modes clears `items` then re-inits (`:178-181`).
Infinite scroll: `useInfiniteScroll(ref1, …, {distance:30})` advancing `currentPage` while `currentPage < Math.ceil(tableData.count / limit)` with `limit = 50`; `tableData` is **never assigned** (the assignment is inside the commented-out `getData`, `:211-226`), so `tableData.value.count` is `undefined` and this loop never fires. A second `useIntersectionObserver` on `.last` elements also increments (`:242-250`).
Row detail: `openModalFn(value, item)` stores `{item, factor_code: item.factor_Code}` and opens a 90 %-width modal rendering `AccountingDetail` with `flag="1"` (`:44-52`, `:108-114`); footer button `بازگشت`.
A full ant `Table` with 5 columns (نوع / تاریخ / فاکتور / مانده طلایی / مانده ریالی) is defined at `:129-168` but its markup is commented out (`:53-65`).
`detail.vue` renders a single-row `Table` from the Pinia store with 11 columns: شماره سند `sh_factor`, تاریخ سند `toJalali(Tarikh,'jYYYY/jMM/jDD')`, نوع سند `no`, نام جنس `NO_kar`, بدهکاری وزنی `tabdil750_kh?.toFixed(2) ?? '0.00'`, بستانکاری وزنی `tabdil750_v`, ته حساب وزنی `Tahesab_t`, بدهکاری مالی `usePrice(rial_kh,true,'')`, بستانکاری مالی `rial_V`, ته حساب مالی `Tahesab_p`, وزن مزنه `MazanehISmesghal?.toFixed(2)` (`:38-123`). All money/weight cells force `dir:'ltr'`. Guard: no `currentReport` → push `home` (`:126-131`).
---
## 7. Store
`src/store/` holds **9 hand-rolled singletons** (plain classes exported as instances) + 1 Pinia store elsewhere. None use Pinia except `src/views/report/store/report.ts`.
| File | Export | Shape | Persisted? | Endpoints |
|---|---|---|---|---|
| `src/store/store.ts` | `useBaseStore()` → singleton `baseStore` | `{config:{about,main_slider}, isLoaded, cats, firstLoadFront, promotionList, ignoreList, priceList, priceCurrentTime, priceCurrentDate}` (`:15-25`, ctor `:30-40`) | no | `GET /config` (`:63`), `GET /promotion` (`:74`), `GET /ignore` (`:86`, dead dup `:98`), `GET /price` (`:114`) — all four fired in parallel by `init()` (`:49-60`) |
| `src/store/myDataStore.ts` | `myStore` (class `myStoreClass`) | `MyData` (see §8.3) + `_activeUser` + `hasActiveChild: Ref<boolean>` | `activeChildToken` in localStorage (`:163,166`) | `POST /me-data` ×2 (`:120-126`, `:130`) |
| `src/store/cartStore.ts` | `cart` (class `cartStore`) | `cartItem[]` | **yes — `localStorage.appCart`** via `useStorage` (`:19-27`) | `POST /get-products {products_id}` (`:41`) |
| `src/store/navStore.ts` | `navStore()` → singleton | `{navBarRoutes, activeLink, showNav, pageTransition}` (`:34-48`) | no | — |
| `src/store/filtersStore.ts` | `query` (a bare `ref<Query>`), `defaultQuery` | `Query` (§8.5) | no | — |
| `src/store/tokenManage.ts` | `userToken`, `userChildToken`, `getUserToken` | pure localStorage readers | reads `appToken`, `activeChildToken` | — |
| `src/store/loginToken.ts` | `token` (class `tokenStore`), `useOtpToken()` | `{token, fireBaseToken}` | no | — |
| `src/store/loadingStore.ts` | `loading` | `{loading: boolean}` | no | — **imported once (`productList.vue:29`) and never read** |
| `src/store/transitionStore.ts` | `transition` | `{transitionFlag, transitionEnded, headerHeight}` (`:24-28`) | no | — |
| `src/store/windowData.ts` | `windowData` | `{width, pullingDown}` | no | — |
| `src/views/report/store/report.ts` | `useReportStore` (**Pinia**, id `'report'`) | `{currentReport: Hesab \| null}` | no | — |
**Persisted state total:** `appToken`, `activeChildToken`, `appCart`, `version` (`src/App.vue:66,71`), plus `sessionStorage.editCustomer` (`customerChoice.vue:149`, consumed `addUser.vue:251-255`).
**`myStore.setValue`** (`src/store/myDataStore.ts:93-115`) coerces `filterGram` and `filterPercentPay` to numbers, writes to `_state` or `_activeUser` depending on `hasActiveChild`, then **mutates the module-level `defaultQuery`** with the user's gram/percent bounds and replaces `query.value` — so filter bounds are user-specific and reset on every profile load.
**`myStore.init()`** (`:117-146`): if parent token → `noHeaderAxios POST /me-data` with an explicit parent `Authorization` header, `setValue(data, isFromInside=true)`; if a child token exists → `axios POST /me-data` (which will use the child token) and `toggleActiveUser(data)`; else `store.init()`. Called from `src/App.vue:126-129` (`onBeforeMount`, unconditional — the `isUserLoggedIn()` guard is commented out at `:127`).
All getters return `readonly(...)` proxies (`store.ts:45-47`, `myDataStore.ts:77-84`, `cartStore.ts:32-34`, etc.), so mutation must go through the setters.
---
## 8. Shared utils, validators, constants
### 8.1 Formatters
| Function | Behaviour | Evidence |
|---|---|---|
| `$filters.price(val)` | `String(val).replace(/\B(?=(\d{3})+(?!\d))/g, ',')`; falsy passthrough | `src/Mixins/filters/index.ts:4-10` |
| `$filters.priceUnit(val)` | same separator + `' تومان '` suffix; guards `typeof ∈ {string,number}` | `:11-16` |
| `$filters.priceUnitRialLess(val)` | separator + a trailing space, no unit | `:17-22` |
| `$filters.jdate(val)` | `moment(val,'YYYY-MM-DD HH:mm:ss').format('jYYYY/jMM/jDD')` | `:23-28` |
| `usePrice(price, digit=true, unit='ریال')` | separator (when `digit`) + `' ' + unit` | `src/utils/usePrice.ts:1-7` |
| `useTowDecimal(n)` | regex-truncates to ≤2 decimals `^-?\d+(?:\.\d{0,2})?` (**truncate, not round**) | `src/utils/usePrice.ts:9-13` |
| `useThreeDecimal(n)` | same with ≤3 decimals | `:14-18` |
| `useFormat(n, unit='')` | `usePrice(Math.abs(+useTowDecimal(n)), true, unit)` — **drops the sign** | `:20-22` |
| `momentTimeAgo(dt)` | `moment.utc(dt).fromNow()` (fa locale) | `src/utils/useDataTime.ts:8-10` |
| `toJalali(dt, fmt='jYYYY/jMM/jDD-hh:mm:ss')` | — | `:12-14` |
| `toJalaliNoTime(dt, fmt='jYYYY/jMM/jDD')` | — | `:16-18` |
| `toTime(dt, fmt='HH:mm')` | — | `:20-22` |
Jalali locale is installed globally once: `moment.locale('fa', fa)` + `moment.loadPersian()` (`src/utils/useDataTime.ts:5-6`). Filters are exposed app-wide as `app.config.globalProperties.$filters` (`src/main.ts:25`); `$baseRoute` and `$prefix` (`'henza'`) likewise (`src/main.ts:26-27`; prefix defined `themeConfig/themeConfig.ts:12`).
**Persian↔English digit conversion: ABSENT.** No `toEnglishDigits`/`convertDigits`/`۰۱۲…` mapping exists anywhere in `src/`. Numeric inputs are native `type="number"` / ant `InputNumber`, so Persian keyboard digits are not normalised.
**Input masks / live thousand separators: ABSENT.** Separators are applied only on **display** via the formatters above.
### 8.2 Validators — `src/Mixins/validator/index.ts`
Base message builder: `message(field) = ' مقدار ${field} نا معتبر است '` (`:1-3`).
| Validator | Rule | Message |
|---|---|---|
| `mobile(f)` | `/^(\+98\|0098\|98\|0)?9\d{9}$/g` | `' مقدار {f} نا معتبر است '` |
| `phone(f)` | `/^((\+98\|0098\|98\|0)\d1)?\d{8}$/g` | same |
| `fa(f)` | `/^([\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C ])+$/` | `' مقدار {f} باید فارسی باشد '` |
| `mail(f)` | `{type:'email'}` | `' مقدار {f} نا معتبر است '` |
| `url(f)` | `{type:'url'}` | same |
| `username(f)` | `/^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{0,29}$/igm` | `' مقدار {f} باید شامل حروف و اعداد انگلیسی و _ باشد '` |
| `required(f)` | `{required:true}` | `' لطفا {f} را وارد کنید '` (**two spaces after لطفا**) |
| `sheba(f)` | `/^IR\d{24}$/igm` | `' مقدار {f} نا معتبر است '` |
| `card(f)` | `/^\d{16}$/igm` | same |
| `zip(f)` | `/^\d{10}$/igm` | same |
| `num(f)` | `/^\d*$/igm` | same |
| `len(f, n)` | `{len:n}` | `' مقدار {f} باید {n} کارکتر باشد '` (`کارکتر` misspelled) |
| `max(f, n)` | `{max:n}` | `' مقدار {f} باید کمتر از {n} کارکتر باشد '` |
| `min(f, n)` | `{min:n}` | `' مقدار {f} باید بیشتر از {n} کارکتر باشد '` |
`dynamic(name, key, data?)` dispatches by string key (`:104-110`). **`sheba`, `card`, `zip`, `fa`, `mail`, `url`, `username`, `num`, `len`, `max`, `min` are never used by any view** — only `required`, `mobile`, `phone` are.
### 8.3 `MyData` (user object) — `src/store/myDataStore.ts:14-39`
`address, api_token, filterBrand: Brand[], filterColor: Color[], filterGram: number[], filterPercentPay: number[], sts_verify_step_two?, sts_verify_step_one?, name, mobile, remain, grams_sell, id, sts, percent, reagent:{name,mobile}, to: string[], rial_remain, type, favorits: number[], mcode: string|null`.
Defaults (`:48-70`): `id: -1`, all numerics `0`, arrays empty, `mcode: null`. Views additionally read `image`, `fname`, `lname`, `phone`, `birth_date`, `marriage_date` — **not declared on the interface**.
### 8.4 Enums / constants with exact values
**User `type`** — never centrally defined; inferred from usage:
| value | Meaning | Evidence |
|---|---|---|
| 1 | کارشناس فروش (sales rep) — can impersonate, see child orders, add customers | `Dashboard.vue:37,88,118,697,706`; `profile.vue:22,57` |
| 2 | مشتری (customer) | `Dashboard.vue:423` (`type === 2 ? 'مشتری' : 'مدیر فروش'`) |
| 4 | بنکدار (wholesaler) — must not see wage; forces `sts=-1` on create | `myDataStore.ts:86-90` (comment "بنکدار (type 4) نباید اجرت ببیند"); `addUser.vue:152` |
| 5 | کارگاه/workshop — custom-order confirm & ship buttons; direct `makeOrderStatus` tile | `Dashboard.vue:134`; `makeOrderStatus.vue:68` |
**Order status (`sts`)** — the 8 tab keys in §6.10 (`Orders.vue:259-270`), with the 4-step rail mapping at `:246-257` and the colour map at `:306-326`.
**Findoc / transaction type** — `{1:'آب شده', 2:'حواله', 3:'چک', 4:'پولی'}` (`addTransaction.vue:184-189`); plus a magic `type == 9` meaning "has no downloadable factor" in the list (`transactions.vue:249-251`).
**Custom-order stages** — 5 Persian strings → 1..5 (`makeOrderStatus.vue:147-164`), see §6.17.
**Promotion / ignore `type`** — `1 = product`, `2 = category(collection_id_id)`, `3 = customer`, `4 = brand` (`src/Mixins/product/index.ts:12-23`, `:33-38`).
**Price slugs** (`src/store/store.ts:118-119`):
- `quotePriceKey = ['mesghal_gold','gold18','gold24','sekebaharazadi','sekeemami','nimseke','robseke','geramiseke']`
- `otherPriceKey = ['usd','eur','ons_gold','ons_silver','ons_platinum','ons_palladium']`
- a superseded list is commented at `:117`: `['geram18','mesghal','nim','nim_down','price_dollar_rl','rob','rob_down','sekeb','sekee_down','silver_999']`
**Tehran districts** — `منطقه 1` … `منطقه 22` with `key: 1..22` (`addUser.vue` regions array, excerpt `:~170-330`).
**Theme colours** (`themeConfig/colors.json`): `primary #1C2D56`, `primaryLighten #1C3362`, `secondary #8D6A46`, `light #f9f9f9`, `grey #898989`, `danger #E31A1A`, `lightGray #A5A5A5`, `lightGray1 #CACED8`, `lightGray2 #E8EAEE`, `notify1 #F4C57A`, `notify2 #916F48`, `info #335CB1`, `green #16a34a`, `red #dc2626`, plus a gray-50..400 ramp.
**KYC levels: ABSENT** — see §5.3.
### 8.5 `Query` (search filter payload) — `src/Mixins/interfaces.ts:68-78`, defaults `src/store/filtersStore.ts:4-14`
| Key | Type | Default |
|---|---|---|
| `colors` | `number[]` | `[]` |
| `brands` | `number[]` | `[]` |
| `ciny_id` | `number` (declared) / `number[]` (assigned at `collection/index.vue:73`) | `[]` |
| `limit_percent` | `number[]` | `[]` (overwritten from `MyData.filterPercentPay`) |
| `limit_gram` | `number[]` | `[]` (overwritten from `MyData.filterGram`) |
| `has_discount` | `boolean` | `false` |
| `text` | `string \| undefined` | `undefined` |
| `cats` | `number[] \| undefined` | `undefined` |
| `page` | `number` | `0` |
| `limit` | *(not on the interface)* | set to `40` ad-hoc at `collection/index.vue:74` |
### 8.6 Other mixins
| File | Export | Purpose |
|---|---|---|
| `src/Mixins/checkAccess.ts` | `isUserLoggedIn()` | `localStorage.getItem('appToken')`; `canNavigate`, `getHomeRouteForLoggedInUser` are dead (`:1-11`) |
| `src/Mixins/favorite.ts` | `toggleFavorite(id)`, `isInFavoritsList(id)` | optimistic toggle + `POST /add-to-fav`; membership check guards against non-array `favorits` (`:14-19`) |
| `src/Mixins/getProducts.ts` | `getProducts`, plus internal `getData`/`getCollection`/`getRandomItems` | §6.19 |
| `src/Mixins/product/index.ts` | `getPromotion`, `getSelectedVariant`, `getWage`, `getHasDiscountWage`, `useProduct` | §6.7 |
| `src/Mixins/useRefresh.ts` | `useRefresh()` | wraps `useRegisterSW()`; `watchEffect` auto-calls `updateServiceWorker()` whenever `offlineReady \|\| needRefresh` — i.e. **silent forced SW update** (`:16-18`) |
| `src/Mixins/fireBase.js` | `useFirebase()` | FCM init with a **hard-coded apiKey/VAPID key** (`:8-18`); registers `${$BasePath}/firebase-messaging-sw.js`; `alert()`s on failure (`:30,35`). **Disabled** — call commented at `src/App.vue:105` |
| `src/Mixins/interfaces.ts` | `category, Ignore, Variant, Brand, Color, Image, Product, cartItem, Query, slide, worldPrices` | §11 |
| `src/Mixins/filters/filters sample.js` | — | sample/scratch file, unused |
---
## 9. Components
Excluding the two vendored trees. 40 app components.
### 9.1 Inputs & form controls
| Component | Notes | Evidence |
|---|---|---|
| `src/components/input/otpInput.vue` | Container. Props: `defaultFocus`, `numInputs` (default 4), `separator`, `inputClasses`, `inputType` (validator: `number\|tel\|password`), `shouldAutoFocus`, `placeholder`, `disabled`. Emits `on-change`, `on-complete`. Key handling: BACKSPACE 8 → clear + focus prev, DELETE 46 → clear, LEFT 37 / RIGHT 39 → move. Paste splits across boxes and validates digits when `inputType==='number'`. `on-complete` fires when `otp.join('').length === numInputs`. | `:10-14,15-45,46-49,139-161,103-124,65-70` |
| `src/components/input/SingleOtpInput.vue` | One box: `min=0 max=9 maxlength=1 pattern="[0-9]"`, truncates to 1 char, allows only 0-9 + `[8,9,13,37,39,46,86]`, selects on focus, auto-focus on mount when `focus && shouldAutoFocus`. | `:3-20,55-61,63-78,82-85,105-110` |
| `src/components/input/Date.vue` | Jalali display shim over `vue3-persian-datetime-picker`; shows `تاریخ :` + `روز :`, falls back to `تعیین نشده`. **Bug:** it never renders the actual picker component, so it can't set a value. Superseded by `InputDatePicker`. | `:4-13,43-52` |
| `src/components/DatePicker/InputDatePicker.vue` | **The real date input.** Wraps the vendored ant `DatePicker` with `fa_IR` locale + `inputReadOnly`, `size="large"`. Setter converts the Jalali `Dayjs` to `val.calendar('gregory').locale('en').format('YYYY/MM/DD')` before emitting; getter re-wraps a string via `dayjs(props.value)`. | `:2-8,12-13,30-45` |
| `src/components/BaseInput.vue` | Generic wrapper input. | — |
| `src/components/search/SearchInput.vue` | Prop `placeHolder`; emits `search`. **Debounced 700 ms** via `useDebounce`, plus Enter and icon-click. Clear "×" (a rotated `add` icon) resets to `undefined` and re-emits. | `:3-10,24-38` |
| `src/components/search/Filters.vue` | §6.19. | — |
| `src/components/search/toggleMore.vue` | "show more" toggle inside filters. | — |
| `src/components/dropDown.vue` | Collapsible section used by `Filters`. | — |
| `src/components/ToggleBar.vue` | Segmented toggle. | — |
> **There is no numeric/price input component.** No mask, no live separator, no digit conversion — numbers use ant `InputNumber` / `<Input type="number">` directly. See §8.1.
### 9.2 Uploaders
| Component | Notes | Evidence |
|---|---|---|
| `src/components/SingleImageUploader.vue` | ant `Upload` `list-type="picture-card"`, `:show-upload-list="false"`, custom `req` posting `FormData{file, has_thumb:'0', has_watermark:'0'}` to the absolute `…/api/file/create`. `beforeUpload` accepts **only** `image/jpeg` / `image/png` → error `فقط تصاویر با فرمت jpeg یا png قابلیت آپلود دارند.` (size check commented out, `:134-137`). Success `بارگذاری فایل با موفقیت انجام شد` / `فایل با موفقیت آپلود شد`; failure `خطا در بارگذاری اطلاعات` / `خطا در آپلود فایل`. Emits `done(data.id)`. Prop `size` default `'128px'`, bound through `v-bind(size)` CSS. `fixUpload()` strips the `capture` attribute on mount + on change so the file picker isn't forced to the camera. | `:2-18,99-123,129-139,33-41,55-57` |
| `src/components/Uploader/MultiImageUploader.vue` + `src/components/Uploader/utils/index.ts` | Multi-image variant. | — |
| `src/components/UploadPic.vue` | Legacy uploader; referenced only in a comment (`editUser.vue:22-23`). | — |
### 9.3 Product
`product/cartItem.vue` (§6.8), `product/order.vue` (order summary card + colour map, §6.10), `product/orderItem.vue` (line item: تعداد/وزن/اجرت درصد/توضیحات), `product/productList.vue` (renders collection chips when route is `search`, then `ProductVerticalFull` rows inside `ScrollFetchSearch`), `product/productHorizontal.vue`, `product/productVertical.vue`, `product/productVerticalFull.vue`, `product/category.vue`, `product/editUser.vue`.
### 9.4 Feedback / overlays
| Component | Notes |
|---|---|
| `DialogVant.vue` | vant dialog with `img` / `title` / `button` slots, native confirm/cancel buttons hidden via CSS (`:83-85`); buttons laid out as a 2-col grid. **Bug:** `watch(() => props.modelValue, …)` but `modelValue` is not declared in `defineProps` (`:54-66`). |
| `DialogVantOneButton.vue` | Same, single-button layout (used by Register, completeProfile, makeOrder, App version dialog). |
| `NotifyVant.vue` | vant notify with an info icon; props `timeOut`, `text`, `color` (a raw CSS string like `'background-color: #DC2626'`), default slot. |
| `Notification.vue`, `customNotification.vue` | Legacy notification components (superseded by the vant ones). |
| `spinner.vue` | ant `Spin` with a custom Henza-logo SVG indicator, or a 3-ring CSS spinner when `indicator` is true. Props `tip`, `size` (default `'large'`), `cls`, `delay`, `spinning`, `indicator`. |
| `empty.vue` | ant `Empty`, `:image` prop, **hard-coded description `موردی یافت نشد`**, `200×200`, `opacity-70`. |
| `WorkActionSheet.vue` | Bottom sheet with `title` / `body` slots (city & region pickers). |
### 9.5 Layout / list plumbing
`ui/container.vue` (`px-[16px] max-w-screen-md m-auto`), `ui/navigation.vue` (§4), `ui/ScrollFetch.vue` (`useInfiniteScroll` distance 50, emits `fetch`, renders `Spinner` + `Empty`), plus near-identical `ScrollFetchSearch.vue`, `ScrollFetchAfterSearch.vue`, `ScrollFetchNotification.vue`. `appHeader.vue` (fixed primary header with rounded corner), `content.vue`, `viewAll.vue`, `slider/homeSlider.vue`, `slider/slider.vue`.
### 9.6 Icons
`src/assets/icons/Icon.vue` + `src/assets/icons/icons.json`. All icons are referenced by the string id `vuesax-linear:<name>` (e.g. `vuesax-linear:bag-happy`, `:home-2`, `:search-normal-1`, `:user`, `:add-square`, `:minus-square`, `:trash`, `:document-download`, `:refresh-2`, `:coin`, `:number-18`, `:number-24`, `:usd-coins`, `:moneys`, `:logout`, `:3d-cube-scan`, `:box-tick`, `:document-text-1`).
### 9.7 Gold price ticker component
**ABSENT as a component.** The price list is inlined in `Dashboard.vue:221-263` and has no ticker/marquee/auto-refresh behaviour. See §10.
---
## 10. Live price / sockets / polling
**Verdict: there is no live price.** No WebSocket, no SSE, no polling timer, no push subscription for prices.
Two independent, coexisting implementations both do a **one-shot `GET /price`**:
**(A) `baseStore.getPriceList()` — `src/store/store.ts:107-156`**
1. `GET /price` → an **object keyed by slug**, converted with `Object.values(data)` (`:126`).
2. Filter rows whose `slug` ∈ `quotePriceKey ∪ otherPriceKey` (§8.4) (`:127-130`).
3. Order = quotes first, then others (`:135`).
4. Map to `{title: x.title.replace('انس',''), price: x.p}` (`:138-143`).
5. `setPriceList(values)`.
6. `data.uaj?.replace(' ','')?.split('-')` is parsed into `uaj` — but the two assignments `prices.date = uaj[0]` / `prices.time = uaj[1]` are **commented out** (`:145-147`), so `setPriceData('', '')` always writes empty strings. The Dashboard's date/time header (`:231-232`) is therefore permanently blank.
**Triggers:** `store.init()` on app boot / after login / after impersonation switch (`src/store/store.ts:49-60`; `myDataStore.ts:132,168`), and the manual refresh icon `vuesax-linear:refresh-2` on the Dashboard (`Dashboard.vue:236` → `getPrices()` `:671-685`), which shows a `van-loading` spinner and one of the two toasts `قیمت ها با موفقیت به روز رسانی شد` / `خطا در به روز رسانی`.
**(B) `Dashboard.getListPrice()` — `src/views/user/src/components/Dashboard.vue:632-660`**
Called once in `onMounted` (`:658-660`). Normalises the payload to an array (handles array / object / scalar), attaches an icon per **index** from a 10-item array (`:612-623`), then **`.reverse()`s** the whole list (`:649`). This is what the visible `listPrice` grid renders (`:243-261`), showing `usePrice(val.p, true, '')` + a `ریال` suffix and an optional `link` making the row clickable (`window.open`, `:453-455`).
So the on-screen list comes from (B), while the manual refresh button re-runs (A) — **the refresh button does not update the list the user is looking at.** The older (A)-driven `priceListArray` computed with its 14 hard-coded icon slots still exists at `:457-532` but is not rendered.
**Refresh cadence for everything else:** none. The only periodic timers in the app are the OTP countdown (`Login.vue:122-131`) and the `window.setInterval(reload, 1000)` logout/refresh pattern (§5.5).
---
## 11. Types (`src/type/*`) + `src/Mixins/interfaces.ts`
`src/type/` contains **interfaces only — no enums anywhere in the project** (`export enum` returns zero hits).
| File | Export | Shape |
|---|---|---|
| `src/type/table.ts` | `TableOutput<T>` | `{count: number, page: number, rows: [T[]], sum: any[]}` — note `rows` is typed as a **1-tuple of arrays**, which is why `index.vue` uses `data.result` instead (`:1-6`) |
| `src/type/accounting.ts` | `Accounting` | 26 fields: `id, factor_Code, mcode, sh_factor, tarikh, geram_v, geram_kh, rial_v, rial_kh, tahesab_T, tahesab_P, rec, reg, insertDTime, editDTime, arz, jens_Felez, gid, rial_V2, rial_Kh2, tahesabP2, arz2, check, vaznPlastik` (`:1-26`) |
| `src/type/hesab.ts` | `Hesab` | 73 fields, PascalCase legacy accounting record. Key ones: `ID, Factor_Code, mcode, sh_factor, Tarikh, vazn, ayar, tabdil750_v, tabdil750_kh, NO_kar, no, rial_V, rial_kh, Tahesab_t, Tahesab_p, Sharh, Status, MazanehISmesghal, SanadType, Darsad_Talayee` (`:1-73`) |
| `src/type/hesabMoshtari.ts` | `HesabMoshtari` | 75 fields, customer ledger. Annotated: `gorooh //گروه`, `tahesabriali //ته حساب مالی 2 - ته حساب ارزی`, `JamV_T //(بدهی طلایی) ته حساب طلایی`, `JamV_P //ته حساب مالی (بدهی ریالی)`, `Tvazn //راس`, `TMablagh //راس مالی`, `Arz_TEMP_1 //واحد ته حساب 1`, `Arz_TEMP_2 //واحد ته حساب مالی 2` (`:1-76`) |
`src/Mixins/interfaces.ts` holds the domain types:
| Interface | Fields |
|---|---|
| `category` | `total, icon:{image}, title, id, collection_id_id, image:{image}` (`:3-14`) |
| `Ignore` | `id, type, item_id` (`:15-19`) |
| `Variant` | `grams?, id?, title?, percentage_pay?` (`:21-26`) |
| `Brand` | `id, title, val?` (`:28-32`) |
| `Color` | `id, title` (`:34-37`) |
| `Image` | `image:{image:any}` (`:39-43`) |
| `Product` | `min_gram?, body_html?, thumb:{full}, title?, images?, id?, variants?, category?, brand?, has_warranty?, is_exist?` (`:45-59`) |
| `cartItem` | `pid, product, qty, vid` — **`note` is used everywhere but not declared** (`:61-66`) |
| `Query` | §8.5 (`:68-78`) |
| `slide` | `text, type, src, link` (`:80-85`) |
| `worldPrices` | `title, price, icon` (`:87-91`) |
`ProductsData` is imported by 3 views (`newest.vue:28`, `searchPage.vue:102`, `collection/index.vue:51`) but is **only defined locally inside `src/Mixins/getProducts.ts:5-11`** and never exported — those imports resolve to `undefined` at runtime (harmless, type-only).
`src/env.d.ts` declares the `*.vue` shim and references `vite/client` (`:1-8`).
---
## 12. Env / config / PWA / service worker
### 12.1 Config keys
**No `.env` file, no `import.meta.env`, no `VITE_*` variables.** Everything is literal in `themeConfig/themeConfig.ts` (see §2.1). `vite.config.ts:91-93` sets `define: {'process.env': {}}` so `process.env.NODE_ENV` in `themeConfig.ts:50` evaluates against that stub.
Hard-coded secrets/URLs in source:
- Firebase config + `apiKey: "AIzaSyDCyHlmR6-aLoqbspmhhRafXVh_LDawcE0"` and VAPID key `BDJLRBku…` (`src/Mixins/fireBase.js:8-18`)
- Absolute API URLs in `SingleImageUploader.vue:107`, `addUser.vue:460`, `Orders.vue:334`, `transactions.vue:225`
- WhatsApp number `989229425856` (`index.html:59`)
### 12.2 Vite
`base: '/'` (`vite.config.ts:11`); dev server `port: 3030`, `https: false` (`:12-13`) though `vite-plugin-mkcert` is registered (`:17`). Aliases §2.2. `resolve.extensions: ['*','.js','.vue','.json','.ts','.tsx']` (`:55`). Less `modifyVars: $themeLessVars` + `javascriptEnabled: true` (`:78-85`). `build: {outDir: 'dist', manifest: true}` (`:87-90`).
### 12.3 PWA (`vite-plugin-pwa`, `vite.config.ts:18-52`)
| Option | Value |
|---|---|
| `mode` | `"development"` — **shipped in dev mode**, `:19` |
| `base` | `/` |
| `srcDir` / `filename` | `src` / `sw.ts` |
| `strategies` | `injectManifest` |
| `includeAssets` | `["/favicon.png"]` |
| `manifest.name` / `short_name` | `HenzaGold` / `HenzaGold` |
| `manifest.theme_color` / `background_color` | `$themeLessVars.primary` = `#1C2D56` |
| `manifest.description` | `HenzaGold` |
| `manifest.start_url` | `/` |
| `manifest.display` | `standalone` |
| `manifest.icons` | `icon-192.png` 192×192; `/icon-512.png` 512×512; `icon-512.png` 512×512 `purpose:"any maskable"` (inconsistent leading slash on the 2nd) |
### 12.4 Service worker — `src/sw.ts` (7 lines, complete)
```ts
import { precacheAndRoute } from 'workbox-precaching'
declare let self: ServiceWorkerGlobalScope
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting()
})
precacheAndRoute(self.__WB_MANIFEST)
```
Precache only. **No runtime caching, no offline fallback, no background sync, no push handler.** (`src/sw.ts:1-7`)
A separate FCM worker at `${$BasePath}/firebase-messaging-sw.js` would be registered by `useFirebase()` (`src/Mixins/fireBase.js:20`), but that call is disabled (`src/App.vue:105`).
### 12.5 Update flow
- `useRefresh()` (`src/Mixins/useRefresh.ts`) is called in `App.vue`'s `onMounted` (`:52-55`) and silently applies any waiting SW as soon as `offlineReady || needRefresh`.
- Independently, `checkVersion()` compares `localStorage.version` against `pkg.version` (`1.0.7`); a mismatch opens a one-button Lottie dialog reading `نسخه جدید هنزا منتشر شد!` with the button `به روز رسانی` → write the new version, `updateServiceWorker()`, `window.location.reload()` (`src/App.vue:22-38,56-74`). First run just seeds the key silently (`:65-67`).
### 12.6 App shell
`src/App.vue:4-19`: `<component :is="getLayout">` picks `FullPage` when `route.meta.layout === 'FullPage'`, else `Master` (`:80-85`). The `router-view` is wrapped in `<keep-alive><suspense>` keyed by `route.name` (`:7-16`) — **so every visited page stays mounted**; the page `<transition>` is commented out (`:6`, `:17`).
`index.html`: RTL is applied on `#app` via CSS (`App.vue:138-148`), `theme-color`/`background-color` `#1C2D56` (`index.html:8-9`), viewport locked `maximum-scale=1.0, user-scalable=no` (`:10`), title `طلای هنزا` (`:11`), a fixed WhatsApp FAB at `z-index: 1000000000` (`:13-47,59-61`). Firebase `<script>` tags are commented out (`:50-52`). Text selection is disabled globally (`App.vue:145-147`); a devtools key-blocker exists but is commented out (`App.vue:113-118`).
---
## 13. Notable defects found while reading (parity-relevant)
| # | Defect | Location |
|---|---|---|
| 1 | Axios error interceptors don't return the rejected promise → failures resolve as `undefined` | `src/plugins/axios.ts:21-23, 29-31, 43-45` |
| 2 | No response interceptor at all → 401/403 unhandled outside `myStore.init()` | `src/plugins/axios.ts` |
| 3 | `wage_weight` and `weight` hard-coded in every order line (`// how to get?!`) | `src/views/cart.vue:181-182` |
| 4 | `addTransaction` submit button is outside the `<Form>` and lacks `html-type="submit"` | `src/views/user/src/components/addTransaction.vue:84-97` |
| 5 | Child-orders tile passes a function reference instead of calling it | `src/views/user/src/components/Dashboard.vue:119` |
| 6 | Report-sheet gram/rial boxes read crossed indices and use the wrong formatter for each | `Dashboard.vue:182-192` |
| 7 | `priceCurrentDate`/`priceCurrentTime` are always `''` (assignments commented out) | `src/store/store.ts:145-151` |
| 8 | Two independent `/price` paths; the refresh button updates the one that isn't rendered | `store.ts:107` vs `Dashboard.vue:632` |
| 9 | `completeOrder` sends the same `sts:2` as `confirmOrder` | `makeOrderStatus.vue:212, 234` |
| 10 | `window.setInterval(reload, 1000)` used where `setTimeout` was meant (4 sites) | `Dashboard.vue:788`, `completeProfile.vue:178`, `makeOrder.vue:172`, `makeOrderStatus.vue:169` |
| 11 | `hasActiveChild === true` compares a `Ref` to a boolean → always false | `profile.vue:23` |
| 12 | Findoc `to` radio binds the array **index**, not the destination string | `addTransaction.vue:50`, `addFindoc.vue:54` |
| 13 | `testBind()` returns `undefined`, so `test.value` becomes `undefined` | `transactions.vue:198, 205` |
| 14 | Two uploaders share one `fileList` model; three `done` handlers are empty stubs | `editUser.vue:65,69`, handlers ≈ `:265-278` |
| 15 | Report infinite-scroll condition reads `tableData.value.count`, never assigned | `report/index.vue:234` |
| 16 | Routes 5/21/22 lack `meta.title` → analytics posts `/log/create/undefined` | `router/index.ts:64-71,200-215`; `customAnalytics.ts:19` |
| 17 | `vite-plugin-pwa` `mode: "development"` in the committed config | `vite.config.ts:19` |
| 18 | Global mobile/phone regexes use the `g` flag (stateful `lastIndex`) | `src/Mixins/validator/index.ts:6,12` |
| 19 | `priceUnit` appends **تومان** to a field named `rial_remain` | `filters/index.ts:13` vs `Dashboard.vue:78` |
| 20 | `singleProduct` fetches the product twice when both `id` and `props.id` are set | `singleProduct.vue:283, 304` |