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>
1179 lines
102 KiB
Markdown
1179 lines
102 KiB
Markdown
# 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` |
|