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>
102 KiB
Inventory — henza_pwa (reference user app)
Scope:
E:\gold bank\henza_pwa. Allpath:lineare relative tohenza_pwa/. Excluded from the file census:node_modules,dist,3.0.0, and two vendored libraries checked intosrc/components/:src/components/DatePicker/DatePicker/**(forked ant-design-vuevc-picker+ Jalali locale) andsrc/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.vueis 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 atsrc/router/index.ts:247-249and never used.isUserLoggedIn()is a purelocalStorage.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
homebeforememory.state.isLoadedis true gets bounced tohome, unless the route setsmeta.skipLoadedCheck(onlyaddUserandcustomerChoicedo).isLoadedbecomes true only when bothconfigandcatsare set (src/store/store.ts:160,165), andcatsis set by the Home view (src/views/home/index.vue:207) — soisLoadedcan only become true after visiting Home. canNavigateandgetHomeRouteForLoggedInUserinsrc/Mixins/checkAccess.ts:1-11are 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, pusheshome, 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 localcatch. - 403: no handling anywhere.
- Manual logout:
Dashboard.vue:781-794— removesappToken,appCart,activeChildToken,toggleActiveUser(undefined), pushhome, thenwindow.setInterval(() => window.location.reload(), 1000)(an interval, not a timeout — reloads in a loop until navigation kills it; same pattern atcompleteProfile.vue:175-181,makeOrder.vue:169-175,makeOrderStatus.vue:166-172). A second, simplerlogOutexists unused atsrc/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_remainbutpriceUnitappends تومان (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 bymandeh[0]?.mandeh > 0 ? text-green : text-red - "ته حساب ریالی(ریال)" renders
useThreeDecimal(mandeh[0]?.mandeh), coloured bymandeh[1]?.mandeh > 0 - The index/colour pairing is crossed in both boxes (
:182,184and:190,192) — gram box shows index 1 formatted as money, rial box shows index 0 formatted as 3-decimals.
- "ته حساب طلایی(گرم)" renders
- "جزئیات حساب" 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)— anignoreListentry suppresses all promotions whentype==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).getCandidPromotionsmatches promotions by the same 4 types;type==3(customer-type discount) is hard-disabled withif (promotion.type == 3) return falseand the Persian comment--- موقتا غیرفعال: تخفیف نوع مشتری (مربوط به ورود/ثبتنام) ---(:31-32).getPromotionpicks the candidate with the maximumdiscount_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 ifvalue !== query.text(:136-141)setCats(id)—cats = [id]orundefined(:142-146)getByQuery()— resetspage = 0then refetches (:132-135)setDefaultQuery()— preservestext, resets everything else todefaultQuery(: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
GET /price→ an object keyed by slug, converted withObject.values(data)(:126).- Filter rows whose
slug∈quotePriceKey ∪ otherPriceKey(§8.4) (:127-130). - Order = quotes first, then others (
:135). - Map to
{title: x.title.replace('انس',''), price: x.p}(:138-143). setPriceList(values).data.uaj?.replace(' ','')?.split('-')is parsed intouaj— but the two assignmentsprices.date = uaj[0]/prices.time = uaj[1]are commented out (:145-147), sosetPriceData('', '')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 keyBDJLRBku…(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 inApp.vue'sonMounted(:52-55) and silently applies any waiting SW as soon asofflineReady || needRefresh.- Independently,
checkVersion()compareslocalStorage.versionagainstpkg.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 |