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

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

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

[ref: henza_server, henza_admin, henza_pwa]

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

102 KiB
Raw Permalink Blame History

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 InputNumbers.

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 SingleImageUploaders 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 FormItems 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)

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