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

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

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

[ref: henza_server, henza_admin, henza_pwa]

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

51 KiB
Raw Blame History

Inventory — apps/user-app (target user app v1)

All path:line references are relative to apps/user-app/. Scope: src/**, root config. node_modules/dist excluded.


1. Stack & versions

Source: package.json:1-38

Field Value Line
name / version / private user-app / 0.0.1 / true package.json:2-4
scripts dev: vite, build: vue-tsc -b && vite build, lint: eslint "src/**/*.{ts,vue}" package.json:6-8

Dependencies (package.json:10-24)

Package Version Line
@ant-design/icons-vue ^6.1.0 package.json:11
@gold-bank/shared-types workspace:* package.json:12
@vueuse/core ^9.13.0 package.json:13
ant-design-vue ^3.2.20 package.json:14
gsap ^3.9.1 package.json:15
moment-jalaali ^0.10.0 package.json:16
pinia ^2.2.0 package.json:17
swiper ^7.4.1 package.json:18
vant ^4.0.3 package.json:19
vue ^3.5.0 package.json:20
vue-router ^4.4.0 package.json:21
vue3-lazyload ^0.2.5-beta package.json:22
@lottiefiles/dotlottie-vue ^0.4.15 package.json:23

devDependencies (package.json:25-37)

Package Version Line
@vitejs/plugin-vue ^5.1.0 package.json:26
autoprefixer ^10.4.2 package.json:27
less ^4.1.2 package.json:28
postcss ^8.4.5 package.json:29
tailwindcss 3.0.18 (pinned) package.json:30
typescript ^5.5.0 package.json:31
vite ^5.4.0 package.json:32
vue-tsc ^2.1.0 package.json:33
vite-plugin-pwa ^0.20.5 package.json:34
workbox-precaching / workbox-window ^7.1.0 package.json:35-36

Notes: gsap is declared but unused in src/** (the GSAP notch nav was replaced — src/components/ui/Navigation.vue:1-10). swiper is used only by src/components/slider/HomeSlider.vue:37 and src/components/slider/Slider.vue:42, both of which have no importers (dead components).


2. HTTP layer

Single file: src/api/client.ts. No axios, no interceptors, no token attach, no refresh, no 401 handling, no error toasts.

Aspect Value Line
Client bare fetch wrapper request<T>() src/api/client.ts:5-15
Base URL const BASE = '/api' (relative; dev-proxied) src/api/client.ts:3
Default headers {'Content-Type':'application/json'} only src/api/client.ts:7
Auth header none — no Authorization/Bearer anywhere in the app src/api/client.ts:5-15
Refresh token none —
401 handling none — any non-2xx throws Error("API <path> failed (<status>): <body>") src/api/client.ts:10-13
Error toast none at the client layer; each caller does console.log(e) or a silent catch e.g. src/views/HomeView.vue:248, src/stores/cartStore.ts:52
Credentials / cookies not set —
Retry / timeout / abort none —

Every API function

Function Method URL Params / payload Returns Line
api.listProducts() GET /api/catalog/products none ProductSummary[] src/api/client.ts:160
api.listCustomers() GET /api/customers none CustomerSummary[] src/api/client.ts:161
api.getCustomer(id) GET /api/customers/{id} path id CustomerDetail src/api/client.ts:162
api.getBalance(customerId) GET /api/accounting/customers/{customerId}/balance path customerId CustomerBalanceDto src/api/client.ts:163
api.getLedgerEntries(customerId) GET /api/customers/{customerId}/ledger-entries path customerId LedgerEntrySummary[] src/api/client.ts:164
api.createOrder(dto) POST /api/orders body CreateOrderDto = {customerId, items:[{productVariantId, quantity, wagePercent}]} OrderSummary src/api/client.ts:165; payload built at src/views/CartView.vue:198-208
api.getCustomerOrders(customerId) GET /api/customers/{customerId}/orders path customerId OrderDetail[] src/api/client.ts:166
api.getLatestGoldPrice(karatPerMille=750) GET /api/pricing/latest?karatPerMille={n} query karatPerMille, default 750 GoldPriceQuote src/api/client.ts:167
api.getHomeSlider() GET /api/home/slider none (public) AdminSlide[] src/api/client.ts:170

Total: 9 API functions. api.getBalance is declared but has zero call sites in src/**.

DTO / response interfaces (src/api/client.ts)

Interface Key fields Line
ProductImageSummary id, url, sortOrder, role: 'FEATURED'|'FEATURED_MOBILE'|'GALLERY'|'GALLERY_MOBILE' 17-22
ProductVariantSummary id, title, rashWeightGrams, defaultWagePercent, qty?, isPreOrder?, imageId?, image? 24-41
CategorySummary id, title, parentId 43-47
BrandSummary id, title 49-52
CollectionSummary id, title, description, isActive 54-59
ProductSummary id, title, description, productCode?, variants[], images[], category, brand, collections[] 61-75
LedgerEntrySummary id, currency, direction, sourceType, amount, confirmed, createdAt, note 77-86
CustomerSummary id, fullName, mobile, goldDebtGrams, fiatDebtRial, goldCreditLimitGrams, isActive, isArchived 88-97
OrderSummary id, orderNumber, status, totalGoldWeightGrams, isSuspendedOverCreditLimit 99-105
OrderItemDetail id, productVariantId, quantity, rashWeightGramsSnapshot, wagePercentSnapshot, discountPercentSnapshot, computedWeightGrams, flatRialCharge 107-116
OrderDetail id, orderNumber, status, totalGoldWeightGrams, wageWeightGrams, flatRialCharge, isSuspendedOverCreditLimit, deliveredAt, createdAt, items[] 118-129
CustomerDetail id, fullName, mobile, nationalCode, cityId, goldCreditLimitGrams, goldDebtGrams, fiatDebtRial, verificationStep, createdAt 131-142
GoldPriceQuote id, karatPerMille, pricePerGramRial, source, quotedAt 144-150
AdminSlide image, link 152-157

3. Router — every route

Source: src/router/index.ts:16-45. History: createWebHistory() (:9). scrollBehavior: restore savedPosition on back/forward, else {top:0} (:12-15).

path name component file meta
/home home src/views/HomeView.vue — (Master layout, no guard) — :18
/product/:id singleProduct src/components/product/SingleProduct.vue (props:true) layout: 'FullPage' — :19-25
/newest newestProduct src/views/NewestView.vue layout: 'FullPage' — :26
/cart cart src/views/CartView.vue — (no auth guard; login enforced in-view) — :27
/login login src/views/user/LoginRegisterView.vue layout: 'FullPage' — :28
/user userPage::hasSubRoute src/views/user/DashboardView.vue — (no guard; view renders a signed-out invite) — :29
/orders ordersList src/views/user/OrdersView.vue requiresAuth: true — :30
/transactions transactionsList src/views/user/TransactionsView.vue requiresAuth: true — :31-36
/profile profileData src/views/user/ProfileView.vue requiresAuth: true — :37
/favorites favoritesList src/views/user/FavoritesView.vue requiresAuth: true — :38
/notifications notifications src/views/user/NotificationsView.vue — (no guard) — :39
/search search src/views/search/SearchView.vue — :40
/collection/:id? collection src/views/search/CollectionView.vue — (optional param; title via ?page=) — :41
/ — redirect → {name:'home'} :43
/:pathMatch(.*)* — redirect → {name:'home'} :44

13 named routes + 2 redirects. No meta.title and no meta.level / level requirement anywhere — there is no KYC/level gating in the router.


4. Navigation

Bottom tab bar: src/components/ui/Navigation.vue; item list lives in src/stores/navStore.ts:25-31.

index label name path icon
1 خانه home /home vuesax-linear:home-2 — navStore.ts:26
2 جستجو search /search vuesax-linear:search-normal-1 — navStore.ts:27
3 سبد خرید cart /cart vuesax-linear:bag-happy — navStore.ts:28
4 سفارشات ordersList /orders vuesax-linear:receipt-text — navStore.ts:29
5 پروفایل userPage::hasSubRoute /user vuesax-linear:user — navStore.ts:30
  • Gating: none per tab — every tab is a plain router-link (Navigation.vue:19-28). /orders is gated by the router's requiresAuth and bounces to /login.
  • Visibility: bar is v-show="nav.showNav" (Navigation.vue:12). router.afterEach sets showNav=true + setActiveLink(index) only for the five tab routes, else false (src/router/index.ts:95-101).
  • Active indicator: CSS transform translateX(-(activeLink-1)*100%), negative because the app is RTL (Navigation.vue:40-44).
  • SearchView hides the bar while its filter sheet is open (src/views/search/SearchView.vue:228) and restores it in onBeforeRouteLeave (:262-264).
  • Other nav surfaces: home bar cart/search links src/views/HomeView.vue:25-34; dashboard 4-tile grid src/views/user/DashboardView.vue:184-200; AccountStrip 3 action buttons (src/components/home/AccountStrip.vue:32-43) — AccountStrip has no importer (dead).

5. Guards

Guard Rule Line
router.beforeEach — already-logged-in to.name === 'login' && session.customerId → redirect {name:'home'} src/router/index.ts:56-58
router.beforeEach — auth to.meta.requiresAuth && !session.customerId → {name:'login', query:{redirect: to.fullPath}} src/router/index.ts:59-61
router.beforeEach — transition to.name==='singleProduct' → nav.setPgaTransition('slideDown'), else undefined src/router/index.ts:64-65
router.afterEach resets transition flags (100 ms cycle, inert — no consumer), sets active tab + nav visibility src/router/index.ts:80-102
onBeforeRouteLeave (SearchView) restores nav visibility if the filter sheet is open src/views/search/SearchView.vue:262-264
In-view login gate (Cart) submitOrder() → if !session.customerId open loginNotify dialog instead of posting src/views/CartView.vue:191-195
In-view login gate (Dashboard) goToDetails() → if !session.customerId open data.loginNotify dialog src/views/user/DashboardView.vue:191-195
In-view login gate (product) add-to-cart: isUserLoggedIn() ? addToCart() : (loginNotify = true) src/components/product/SingleProduct.vue:227; helper src/utils/checkAccess.ts:4-6 (localStorage.getItem('appToken'))
Post-login redirect route.query.redirect honoured, else {name:'home'} src/views/user/LoginView.vue:161-162

KYC / verification-level guard: does not exist. verificationStep is read-only display text in src/views/user/ProfileView.vue:32,54 with labels at :75-80. No route, button or flow is gated on it. Credit-limit (goldCreditLimitGrams, isSuspendedOverCreditLimit) is likewise never enforced client-side.


6. Views — all 14 files under src/views (recursive)

File Purpose
src/views/HomeView.vue Redesigned shop-first home: brand bar, live rate strip, hero slider, category rail, featured grid, newest reel, trust badges.
src/views/NewestView.vue "جدیدترین محصولات" grid with incremental reveal.
src/views/CartView.vue Basket → per-item note → confirm → POST order → success step.
src/views/search/SearchView.vue Search + category chips + filter sheet + collection tiles + result grid.
src/views/search/CollectionView.vue Products of one collection (client-side filter by route param).
src/views/user/LoginRegisterView.vue Toggle shell between LoginView and RegisterView.
src/views/user/LoginView.vue Mobile → OTP login (OTP verification stubbed).
src/views/user/RegisterView.vue 7-field registration form (submit stubbed, no endpoint).
src/views/user/DashboardView.vue "میز کار": balance card, 4-tile access grid, rate strip, logout dialog.
src/views/user/OrdersView.vue Order list, 9 status chips, number search, detail drawer with 4-step tracker.
src/views/user/TransactionsView.vue "اسناد" ledger list + detail drawer.
src/views/user/ProfileView.vue Read-only identity card (4 fields + verification seal).
src/views/user/FavoritesView.vue Local favourites filtered against the catalogue.
src/views/user/NotificationsView.vue Static empty state only — no data source.

6.1 HomeView.vue

Aspect Detail Line
API api.listProducts() GET /api/catalog/products + memory.init() (same endpoint) in Promise.all 243-249
API api.getLatestGoldPrice() GET /api/pricing/latest?karatPerMille=750; on throw → goldPrice=null 251-255
API api.getHomeSlider() GET /api/home/slider; on throw → adminSlides=null 257-263
Derived data featured = products[0]; gridProducts = slice(1,5); newestProducts = slice(5,13) 160-162
Hero source admin slides when non-empty (filtered by s.image), else first 3 categories with rotating copy SLIDE_COPY (مجموعه منتزر/دیدن مجموعه, انتخاب سردبیر/مشاهده, پیشنهاد این هفته/بررسی کنید) 170-202
Slide click absolute http(s) link → window.open(link,'_blank','noopener'); other link → router.push(link); else set filters.query.cats=[categoryId] → /search 219-235
Category click filters.query.cats=[cat.id] → router.push({name:'search'}) 214-217
Buttons cart icon (badge = cart.items.length), search icon, همه محصولات→/search, مشاهده همه→/newest, ShowcaseCard→drawer 25-34, 61, 75, 63-66
Pull-to-refresh vant PullRefresh → loadData() 38-42, 266-269
Empty state v-if="!loading && !products.length" → "هنوز محصولی در مجموعه ثبت نشده است." 88-91
Conditional render hero only if slides.length; rail only if memory.cats.length; featured section if featured; newest section if newestProducts.length 46, 48, 56, 70
Detail ant Drawer bottom, height=100vh, hosts SingleProduct 110-119
Not present no price on any card, no form, no validation, no pagination params file header 13-19

6.2 CartView.vue

Aspect Detail Line
API cart.init() on mount + on pull-refresh → api.listProducts() (re-sync/prune) 224-226, 219-222; store src/stores/cartStore.ts:39-55
API api.createOrder({customerId: session.customerId, items:[{productVariantId: item.vid, quantity: item.qty, wagePercent: parseFloat(variant.defaultWagePercent) || 0}]}) POST /api/orders 198-208
Steps step===1 basket, step===2 success 35, 66
Calculation shown total weight only: cart.total() = Σ parseFloat(rashWeightGrams)*qty, .toFixed(2), unit گرم. No rial total. 86-89; src/stores/cartStore.ts:124-131
Form note textarea, rows=4, placeholder "مثلاً سایز یا جزئیات سفارش"; no validation rules 107-112
Note submit cart.updateFeild(note, tempRowIndex, tempRow, 'note'), closes drawer, clears model 176-181
Confirm dialog DialogVant "آیا مطمئن به ثبت سفارش هستید ؟" — "بله"→submitOrder, "بی خیال"→close; timeOut:-1, closeOnClickOverlay 116-125
Login dialog "برای ثبت سفارش ابتدا باید وارد حساب کاربری خود شوید." — "ورود/ثبت نام"→/login, "بیخیال"→close 127-138
Loading Spinner :spinning="cartData.loadingPage" tip "درحال دریافت اطلاعات"; loadingPage true during POST 32, 213, 216
Submit bar rendered only when cart.items.length > 0 && step === 1; button is not disabled while posting (only the Spinner overlays) 82, 91
Success shows cartData.orderID (= order.orderNumber), cart.clear(), CTA "فهرست سفارش‌ها" → /orders then step=1 209-211, 185-188
Error catch { console.log(e) } — no user-visible error message on order failure 212-214
Empty "سبد خرید شما خالی است." + CTA "دیدن محصولات" → /home 56-62

6.3 search/SearchView.vue

Aspect Detail Line
API api.listProducts() on mount and on pull-refresh 206-213, 215-218, 266
Filters (all client-side) text → p.title.includes(text); cats → cats.includes(p.category.id); brands → brands.includes(p.brand.id); limit_gram (len 2) → any variant weight within [min,max] 153-167
Pagination client-side window only, PAGE_SIZE = 20, grown by IntersectionObserver with rootMargin:'400px'; reset to 20 on any filter change. No server page/limit params. 174-199
Chips "فیلتر" (shows ${filterKeys.length} فیلتر when active), "همه" (clears cats), one chip per available category 23-50
Collections tiles → router.push({path:'/collection/'+id, query:{page:title}}) 71-82
Header PageBar title flips to "فیلترها" while the sheet is open; back button closes the sheet instead of navigating 17, 241-248
Nav side-effect opening the sheet hides the tab bar; closing / leaving restores it 226-237, 262-264
Empty "محصولی با این مشخصات پیدا نشد." when !hasMore && !loadingPage 103-106
Result count SectionHead subtitle ${filteredProducts.length} محصول 84-88

6.4 search/CollectionView.vue

Aspect Detail Line
API api.listProducts() on mount 58-65
Filter p.collections.some(c => c.collectionId === route.params.id) — client-side 54-56
Title route.query.page else fallback 'مجموعه' 52
Empty "در این مجموعه محصولی ثبت نشده است." when !loadingPage 26-29
Pagination none —

6.5 NewestView.vue

Aspect Detail Line
API useProducts().getProducts(loadingPage, false) → api.listProducts() 59-61; src/composables/useProducts.ts:24-35
Pagination PAGE_SIZE = 20 window; pull() grows the window first, only then calls getProducts(loadingPull, true) — which is a no-op (isPull returns early) 55-71; useProducts.ts:25
Empty ScrollFetchSearch renders EmptyState when productsData.flag (= list length < 1) src/components/ui/ScrollFetchSearch.vue:19; useProducts.ts:29

6.6 user/LoginView.vue

Aspect Detail Line
API Step 1: api.listCustomers() GET /api/customers; match c.mobile === formModel.username && c.isActive && !c.isArchived 131-133
API Step 2: no request — OTP not verified; session.login(matchedCustomer.id, matchedCustomer.mobile) 153-160
Field username / "تلفن همراه", <Input type="number"> 62-63
Validation required('تلفن همراه') → " لطفا تلفن همراه را وارد کنید "; mobile('تلفن همراه') pattern /^(\+98|0098|98|0)?9\d{9}$/g → " مقدار تلفن همراه نا معتبر است " 61; src/utils/validator.ts:5-8,23,3
Error message not-found → NotifyVant red (#DC2626) "اطلاعات کاربری در سیستم وجود ندارد." (2000 ms) 22, 103, 135-137
OTP OtpInput :num-inputs="4", inputType="tel", auto-focus, @on-complete="sendToken" 40-48
Timer timer = 120 s on step 2, 1 s countdown; label "امکان دریافت مجدد رمز در {counterZeroShow(timer)} ثانیه دیگر" (zero-padded < 10), else "برای دریافت رمز عبور مجدد تلاش کنید" 112-122, 50-53, 170
Button label body.step===1 ? 'دریافت رمز عبور' : 'دریافت مجدد رمز عبور'; :disabled="body.loading || (timer > 0 && body.step === 2)" 66-73, 172
Success push route.query.redirect or /home, then notify "با موفقیت وارد شدید" 161-164
Switch "ثبت نام کنید" → emit('register', true) 77-80
Digit conversion none — no Persian→English digit normalisation on the mobile input —

6.7 user/RegisterView.vue

Field Label Control Validation Line
fname نام Input required('نام') 37-39
lname نام خانوادگی Input required('نام خانوادگی') 40-42
mobile شماره موبایل Input type=number mobile() + required() 43-49
store_name نام فروشگاه Input required('نام فروشگاه') 50-52
phone تلفن فروشگاه Input type=number required('تلفن فروشگاه') only (no phone() pattern) 53-55
address آدرس پستی Textarea rows=3 required('آدرس') 56-58
city شهر Input (plain text, no picker) required('شهر') 59-61
Aspect Detail Line
API none — stubbed. register() only sets registeredInfo = true 117-125
Submit button "ثبت اطلاعات", type=submit, no disabled state 63
Success dialog "ثبت نام شما با موفقیت انجام شد. منتظر تایید اطلاعات ثبت نامی شما توسط کارشناسان بمانید." + "متوجه شدم" → /home; closeOnClickOverlay=false 74-86
Loading Spinner :spinning="profileData.loading" tip "در حال ثبت اطلاعات" 25

6.8 user/DashboardView.vue

Aspect Detail Line
API api.getCustomer(session.customerId) GET /api/customers/{id} (only when signed in) 208-211
API api.getLatestGoldPrice(); on throw → null 212-216
Calculations isDebtor = parseFloat(goldDebtGrams) > 0; grams = Math.abs(parseFloat(goldDebtGrams)); label بدهکار گرمی/بستانکار گرمی; seal بدهکار/بستانکار; rial row price(customer.fiatDebtRial) + "ریال" 176-180, 58-71
Access tiles id 0 سفارشات→ordersList, 1 اسناد→transactionsList, 2 پروفایل→profileData, 3 علاقمندی→favoritesList; all disabled:false 184-200
Gate any tile while signed out → data.loginNotify dialog (not navigation) 191-195
Logout dialog "آیا می خواهید از برنامه خارج شوید؟" → "بله" = session.logout() + push /home; "بی خیال" 123-132, 202-206
Login dialog long invite copy → "ورود/ثبت نام" / "بیخیال" 134-148
Signed-out block invite card with CTA "ورود / ثبت‌نام" 82-93
Pull-to-refresh PullRefresh → loadDashboard() 51, 219-222
Error api.getCustomer is not wrapped in try/catch — a failure rejects loadDashboard() 209-211

6.9 user/OrdersView.vue

Aspect Detail Line
API api.getCustomerOrders(session.customerId) GET /api/customers/{id}/orders 196-204
Filter chips (9) ALL همه, SUSPENDED معلق, PLACED ثبت سفارش, ADMIN_APPROVED تایید مدیر سیستم, SOURCING درحال تامین, PREPARING درحال آماده سازی, DELIVERED_TO_AGENT تحویل مسئول فروش, DELIVERED_TO_CUSTOMER تحویل مشتری, CANCELLED لغو شده 167-177
Search SearchInput placeholder "جستجوی شماره سفارش" → o.orderNumber.includes(term) 32, 188
Filtering client-side (status + number). No server-side query params, no pagination. 185-190
Detail drawer 4-step tracker ORDER_STEPS, headline (orderNumber + coloured status), specs: items count "آیتم", parseFloat(totalGoldWeightGrams).toFixed(2) گرم, jdate(createdAt); items via OrderItemCard 86-138
Status colours from ORDER_STATUS_COLORS (inline background/color styles) 108-113, 210
Empty "سفارشی با این مشخصات پیدا نشد." when !loading 59-62
Pull-to-refresh yes → loadOrders() 51, 218-221

6.10 user/TransactionsView.vue

Aspect Detail Line
API api.getLedgerEntries(session.customerId) GET /api/customers/{id}/ledger-entries 142-150
Source labels ORDER_STOCK سفارش, ORDER_PRE_ORDER سفارش ساخت, REPAYMENT_MELTED_GOLD طلای آب‌شده, REPAYMENT_TRANSFER حواله, REPAYMENT_CHEQUE چک, REPAYMENT_CASH پولی, WALLET_BONUS پاداش, ADJUSTMENT اصلاحیه 112-121
Direction isCredit = direction === 'CREDIT' → green/in arrow-down; else red/out arrow-up 132, 31-33
Amount format currency === 'GOLD_GRAM' ? "{amount} گرم" : "{price(amount)} ریال" 134-135
Confirm state confirmed ? 'تایید شده' : 'در انتظار تایید' 47-49
Detail drawer height 420px, 5 rows: نوع سند / مبلغ / وضعیت / تاریخ / توضیحات (note || '—') 63-97
Empty "سندی برای حساب شما ثبت نشده است." 56-59
Filters / pagination none —

6.11 user/ProfileView.vue

Aspect Detail Line
API api.getCustomer(session.customerId) (skipped entirely when signed out) 88-96
Fields (read-only) نام و نام خانوادگی, تلفن همراه, کد ملی, وضعیت احراز هویت 40-56
Verification labels UNVERIFIED احراز نشده, STEP_ONE_SUBMITTED مرحله اول ثبت شده, STEP_TWO_SUBMITTED مرحله دوم ثبت شده, VERIFIED تایید شده 75-80
Seal class is-ok when verificationStep === 'VERIFIED', else is-pending 31-33, 86
Empty "اطلاعات حساب در دسترس نیست." when !loading 59-62
Editing / uploads none — no edit form, no document upload, no bank fields header 6-17

6.12 user/FavoritesView.vue

Aspect Detail Line
API api.listProducts() on mount 55-62
Filter allProducts.filter(p => favorites.isFavorite(p.id)) — local store, not persisted 53; src/stores/favoritesStore.ts:9-21
Count SectionHead subtitle ${favoriteProducts.length} محصول 19
Empty "هنوز محصولی را به علاقه‌مندی‌ها اضافه نکرده‌اید." + CTA "دیدن محصولات" → /home 25-31

6.13 user/NotificationsView.vue

Static: PageBar "اعلان‌ها" back-to="home" + empty state "اعلانی برای نمایش وجود ندارد." — no API, no store, no props (:9-17).

6.14 user/LoginRegisterView.vue

Shell: showRegister ref toggled by the register event from either child (:7-13).


7. Pinia stores

Store id State Actions Persistence
src/stores/cartStore.ts cart items: CartItem[] {pid, vid, product, qty, note?} (:8-14,32-34) init() (GET /catalog/products, prune dead lines :39-55), addToCart (refuses non-orderable :56-72), capQty (:79-84), maxQty (:87-93), remove, updateFeild (clamps qty :101-114), find_index, isInCart, variant, total() (:124-131), clear() localStorage key appCart, JSON, written by persist() on every mutation (:16-29)
src/stores/session.ts session customerId, mobile (hydrated from localStorage :11-12) setCustomerId, login(customerId, mobile), logout() localStorage keys gold-bank:customerId, gold-bank:mobile, appToken (= customerId, legacy compat) (:20-32)
src/stores/configStore.ts config config.main_slider: Slide[] (3 hardcoded placeholder slides :39-58), cats: HomeCategory[], loaded setCats, init() — derives distinct categories from api.listProducts() (:73-91) none
src/stores/favoritesStore.ts favorites ids: string[] toggle(id), isFavorite(id) none — in-memory only, lost on reload (:1-21)
src/stores/filtersStore.ts filters query: {colors[], brands[], ciny_id[], limit_percent[], limit_gram[], has_discount, text?, cats?, page:0} (:9-31) reset() none
src/stores/navStore.ts nav navBarRoutes[5], activeLink:1, showNav:true, pageTransition:'right' setNavVisibility, setActiveLink, setPgaTransition none
src/stores/loadingStore.ts loading loading:false setLoading none — no importers (dead)
src/stores/transitionStore.ts transition transitionFlag, transitionEnded, headerHeight setTransitionFlag, setTransitionEnded, setHeaderHeight none; transitionFlag is inert (src/router/index.ts:72-79)
src/stores/windowDataStore.ts windowData width:0, pullingDown:false setWidth, setPullingDown none

Unused filter fields (declared, never applied): colors, limit_percent, ciny_id, has_discount, page (src/stores/filtersStore.ts:1-6).


8. Shared utils

src/utils/filters.ts — formatters

Function Behaviour Line
price(val) thousands separator via replace(/\B(?=(\d{3})+(?!\d))/g, ','); returns val unchanged when falsy (so 0 / '' pass through) 7-12
priceUnit(val) same separator + " تومان " suffix 14-19
priceUnitRialLess(val) same separator + trailing space 21-26
jdate(val) moment(val).format('jYYYY/jMM/jDD') — parses ISO 8601 28-36

Digits: Western only. File header explicitly forbids swapping to fa-IR (:1-4). There is no Persian↔English digit converter anywhere in src/**, and no gram/weight formatter — weights are printed raw or via ad-hoc parseFloat(x).toFixed(2) (src/views/user/OrdersView.vue:123, src/components/product/OrderCard.vue:35) or Number(x) (src/components/product/SingleProduct.vue:140).

src/utils/validator.ts — rules (ant-design-vue rule objects)

Rule Pattern / constraint Message Line
message(field) — " مقدار {field} نا معتبر است " 3
mobile /^(\+98|0098|98|0)?9\d{9}$/g message 5-8
phone /^((\+98|0098|98|0)\d1)?\d{8}$/g message 9-12
fa /^([؀-ۿﮊپچگ‌ ])+$/ " مقدار {field} باید فارسی باشد " 13-16
mail type:'email' message 17
url type:'url' message 18
username /^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{0,29}$/gim " مقدار {field} باید شامل حروف و اعداد انگلیسی و _ باشد " 19-22
required required:true " لطفا {field} را وارد کنید " 23
sheba /^IR\d{24}$/gim message 24
card /^\d{16}$/gim message 25
zip /^\d{10}$/gim message 26
num /^\d*$/gim message 27
len(field,len) len " مقدار {field} باید {len} کارکتر باشد " 28
max(field,max) max " مقدار {field} باید کمتر از {max} کارکتر باشد " 29
min(field,min) min " مقدار {field} باید بیشتر از {min} کارکتر باشد " 30

Only required and mobile have call sites (LoginView, RegisterView).

src/utils/orderStatus.ts — enums (exact values)

Const Content Line
ORDER_STATUS_LABELS SUSPENDED:'معلق', PLACED:'ثبت سفارش', ADMIN_APPROVED:'تایید مدیر سیستم', SOURCING:'درحال تامین', PREPARING:'درحال آماده سازی', DELIVERED_TO_AGENT:'تحویل مسئول فروش', DELIVERED_TO_CUSTOMER:'تحویل مشتری', CANCELLED:'لغو شده' 8-17
ORDER_STATUS_COLORS PLACED {#3B82F6,#EFF6FF}, SOURCING {#EAB308,#FEFCE8}, PREPARING {#F97316,#FFF7ED}, DELIVERED_TO_AGENT {#06B6D4,#ECFEFF}, DELIVERED_TO_CUSTOMER {#22C55E,#F0FDF4} — SUSPENDED/ADMIN_APPROVED/CANCELLED deliberately uncoloured 23-29
ORDER_STEPS ['ثبت سفارش','آماده سازی','تحویل مشتری','لغو شده'] 35
orderStepIndex(status) PLACED/ADMIN_APPROVED/SUSPENDED→0; SOURCING/PREPARING→1; DELIVERED_TO_AGENT/DELIVERED_TO_CUSTOMER→2; CANCELLED→3; default 0 37-54

Other constants

Const Value Line
VERIFICATION_LABELS UNVERIFIED/STEP_ONE_SUBMITTED/STEP_TWO_SUBMITTED/VERIFIED → احراز نشده / مرحله اول ثبت شده / مرحله دوم ثبت شده / تایید شده src/views/user/ProfileView.vue:75-80
SOURCE_LABELS ledger source types (see §6.10) src/views/user/TransactionsView.vue:112-121
isUserLoggedIn() localStorage.getItem('appToken') src/utils/checkAccess.ts:4-6
Theme colours primary #1C2D56, secondary #8D6A46, danger #E31A1A, ink #14161A, wine #7A1B33, canvas #FFFFFF, hairline #ECEDEF (+ 30 more) themeConfig/colors.json:1-38
Theme vars prefix: 'montzar', font-size 14px, border-radius-base 5px, height-lg 42px, checkbox-size 20px themeConfig/themeConfig.ts:15-43

9. Components (21 files + 1 icons.json)

File Purpose / API
src/components/icon/Icon.vue Inline-SVG icon from icons.json map; props icon, color?, size?; tempIcon is a computed (bug-fix noted :19-34), applySize() sets width/height after nextTick (:36-47).
src/components/input/OtpInput.vue OTP input. Props defaultFocus, numInputs=4, separator, inputClasses, inputType, shouldAutoFocus=false, placeholder, disabled; emits on-change, on-complete. Keycodes BACKSPACE 8 / LEFT 37 / RIGHT 39 / DELETE 46 (:6-9); paste handling :67-83; completion when joined length === numInputs (:42-47).
src/components/input/SingleOtpInput.vue One OTP cell: maxlength=1, pattern="[0-9]", min=0 max=9, default inputType='tel'; only numeric keycodes + [8,9,13,37,39,46,86] pass (:62-75); focus selects (:79-82).
src/components/search/SearchInput.vue Debounced search field, useDebounce(value, 700) (:42); emits search on enter, icon click and debounce; clear button. Known bug preserved: watcher tests if (value) (the ref object, always truthy) not value.value — clear icon never auto-hides (:8-11, 43-47).
src/components/search/Filters.vue Filter sheet. Brand toggle-chips derived from products (:95-101); gram range via ant Slider range reverse :step="0.01", bounds [floor(min), ceil(max)] of all rashWeightGrams, fallback [0,100] (:103-107), seeded once when limit_gram empty (:114-122). Buttons "پاک کردن فیلتر‌ها" (resetFilters:true) / "اعمال فیلتر" (:75-77, 131-150). Dropped: colour filter, wage-percent range, discount toggle (:18-25).
src/components/ui/DropDown.vue Accordion, props text?, local show ref (:24-30).
src/components/ui/Navigation.vue Bottom tab bar (§4).
src/components/ui/PageBar.vue Sticky bar; props title, back=true, backTo?; slots trailing, below; onBack = router.push({name: backTo}) else router.back() (:34-50).
src/components/ui/Container.vue <main class="px-[16px] max-w-screen-md m-auto ...">; registered globally as container (src/main.ts:31).
src/components/ui/ContentWrapper.vue margin-top from transitionStore.headerHeight — no importers.
src/components/ui/ScrollFetch.vue Infinite scroll via useInfiniteScroll(distance:50), default EmptyState — no importers.
src/components/ui/ScrollFetchSearch.vue Same, favoritEmpty.svg image; used by NewestView.vue:41.
src/components/ui/ScrollFetchAfterSearch.vue Same, ordersEmpty.svg — no importers.
src/components/Spinner.vue ant Spin wrapper; props tip, size='large', cls, delay, spinning, indicator, fixed=true; brand-logo indicator by default.
src/components/DialogVant.vue vant Dialog, 2-column button grid; slots img/title/button; show is not a declared prop — works via attribute fallthrough (:1-10).
src/components/DialogVantOneButton.vue Single-action variant; used by src/App.vue:22.
src/components/NotifyVant.vue vant Notify toast; props modelValue, timeOut, text?, color?; auto-hides after timeOut unless -1 (:34-44).
src/components/EmptyState.vue ant Empty description="موردی یافت نشد", optional image.
src/components/ViewAll.vue "مشاهده همه" router-link — no importers.
src/components/product/SingleProduct.vue Product detail (route + drawer). See §6 notes below.
src/components/product/ProductCard.vue Catalogue card: image (v-lazy), fav toggle, ناموجود / پیش‌سفارش flag, title, {min_gram} گرم, اجرت {wage}٪; opens its own 100vh Drawer with SingleProduct (:53-62).
src/components/product/CartItem.vue Cart line: qty stepper (+ disabled when !canIncrement; at qty 1 the - becomes a trash/remove), note button toggling "توضیحات"/"ویرایش توضیحات" (:32-63, 82-91).
src/components/product/OrderCard.vue Order row: coloured mark + status pill, {items.length} آیتم, toFixed(2) گرم, jdate(createdAt), "جزئیات" (:15-48).
src/components/product/OrderItemCard.vue Order line: image via useOrderItemProduct lookup, {quantity} عدد, {computedWeightGrams} گرم, {wagePercentSnapshot} ٪ اجرت (:30-43).
src/components/product/GallerySlider.vue Pointer-Events swipe gallery; v-model:index; props slides, alt, fit='cover', showCounter=false; emits tap; DRAG_COMMIT_RATIO 0.18, TAP_MAX_PX 6; track pinned direction: ltr for RTL correctness (:74-192).
src/components/product/Category.vue Legacy category tile with id === '45' special-case — no importers (:1-45).
src/components/home/RateStrip.vue Price ticker. Props quote: GoldPriceQuote|null; label "نرخ طلای ۱۸ عیار"; value price(pricePerGramRial.split('.')[0]) + "ریال / گرم"; jdate(quotedAt); placeholder "در انتظار اعلام نرخ" when null (:6-33).
src/components/home/AccountStrip.vue Balance strip with 3 actions — no importers (dead).
src/components/home/CategoryRail.vue Horizontal category chips; emits select.
src/components/home/ShowcaseCard.vue Editorial card; props value, wide?, badge?; fav toggle; weight + wage; no price (:10-48).
src/components/home/ShowcaseSlider.vue Dependency-free hero slider; autoplay 6000 ms (:81-84), pointer drag, DRAG_COMMIT_RATIO 0.18, TAP_MAX_PX 6 (:102-103), dots; ShowcaseSlide {key, eyebrow, title, subtitle, cta, image?, categoryId?, link?} (:55-66).
src/components/home/SectionHead.vue Title + subtitle + optional action button.
src/components/slider/HomeSlider.vue Swiper banner + popup drawer; slide types popup/link/discount; preserved bug: sets query.discount instead of has_discount (:83-92) — no importers.
src/components/slider/Slider.vue Swiper gallery + zoom drawer + GSAP-era SVG curve maths; preserved bug: showPopup(item) called with one arg so index is always undefined (:1-9, 60-67) — no importers.

Missing component categories (do not exist in this app): numeric/price input with thousands-separator masking, amount/gram input, date picker, file uploader, image uploader.

Composables

File Purpose Key detail
src/composables/useProduct.ts Adapts ProductSummary → card contract isVariantOrderable = qty>0 || isPreOrder (:25-27); getCheapestVariant = lowest defaultWagePercent among orderable, fallback all (:29-40); hasDiscountWage always null (:56); has_warranty always false (:64)
src/composables/useProductDetail.ts "Detail" fetch = full list + client-side find (no single-product endpoint) :16-29
src/composables/useProducts.ts List + fake pagination; totalPages always 0; getProducts(loading, isPull=true) returns immediately :24-35
src/composables/useOrderItemProduct.ts Module-level cached Map<variantId, {productTitle, variantTitle, imageUrl}> built from listProducts() :14-31
src/composables/useRefresh.ts Auto-applies a waiting service worker :7-17

Layouts

File Behaviour
src/layout/MasterLayout.vue <router-view/> + <Navigation/> + a primary-coloured overscroll strip; tracks width via useResizeObserver → windowDataStore. Known issue: it renders its own router-view, so App.vue's keep-alive/suspense wrapper is bypassed (:3-7).
src/layout/FullPageLayout.vue Bare <router-view/> — selected by meta.layout === 'FullPage' (src/App.vue:57-62).

10. Live price / sockets / polling

Aspect Finding
Mechanism Pull-on-mount only. api.getLatestGoldPrice() (GET /api/pricing/latest?karatPerMille=750) is called once per view mount.
Call sites src/views/HomeView.vue:252 (+ pull-to-refresh :266-269); src/views/user/DashboardView.vue:213 (+ pull-to-refresh :219-222).
WebSocket / SSE none — no WebSocket, socket.io, EventSource anywhere in src/**.
Polling none — no setInterval refresh for price. The only setInterval is the hero autoplay (src/components/home/ShowcaseSlider.vue:81); the only price-adjacent setTimeout is the OTP countdown (src/views/user/LoginView.vue:115).
Display src/components/home/RateStrip.vue:13-19 — integer part only (split('.')[0]), thousands-separated, ریال / گرم, with Jalali quotedAt.
Karat fixed 750 (18k) default; no karat selector anywhere.
Multi-row ticker explicitly dropped (src/views/user/DashboardView.vue:28-31).

11. Env / config / PWA

Item Value Line
envDir repo root (../..) vite.config.ts:9,15
USER_APP_PORT 5181 (fallback in config), declared in root .env vite.config.ts:11; .env:26
API_PORT 3000 vite.config.ts:12; .env:24
Dev server host 0.0.0.0, strictPort: true vite.config.ts:44-49
Proxy /api → http://localhost:3000, /uploads → same vite.config.ts:50-53
import.meta.env.* no VITE_* variables are read anywhere in src/** —
Less vars modifyVars: $themeLessVars, javascriptEnabled: true vite.config.ts:55-62
PWA strategy VitePWA injectManifest, srcDir: 'src', filename: 'sw.ts', base '/', includeAssets ['favicon.ico'], devOptions.enabled true vite.config.ts:20-27
PWA manifest name/short_name/description Montzar; theme_color/background_color = $themeLessVars.primary (#1C2D56); start_url '/'; display 'standalone'; icons 192 + 512 + 512-maskable vite.config.ts:28-41
Service worker precacheAndRoute(self.__WB_MANIFEST) + SKIP_WAITING message handler src/sw.ts:9-14
Auto-update useRefresh() applies a waiting SW immediately src/composables/useRefresh.ts:8-16
Version dialog localStorage key version vs package.json version → "نسخه جدید منتزر منتشر شد!" + "به روز رسانی" (updateSW + reload) src/App.vue:66-81, 33-38
public/manifest.json { "gcm_sender_id": "464066939468" } only (legacy FCM leftover; not linked from index.html) public/manifest.json:1-3
index.html title طلای منتزر, theme-color #1C2D56, maximum-scale=1.0, user-scalable=no, apple web-app meta index.html:6-11
localStorage keys (all) appCart, gold-bank:customerId, gold-bank:mobile, appToken, version cartStore.ts:16; session.ts:22-25; App.vue:69
Global registrations Pinia, router, vue3-lazyload (loading+error placeholder = montzar-logo.png), global container component src/main.ts:22-31
Style imports tailwind.css → ant-design-vue/dist/antd.less → style.less → vant/lib/index.css src/main.ts:15-18
Docker Dockerfile, nginx.conf, .dockerignore present at app root —

12. CANDIDATE PROTECTED — new features / new design not in a 2023-era reference PWA

These are deliberate additions or redesigns in the target. Treat as protected unless explicitly told otherwise.

12.1 Entirely new pages / routes

Item Path(s) Evidence
Notifications page route /notifications name notifications; src/views/user/NotificationsView.vue src/router/index.ts:39; view header states "New nav destination (bottom nav redesign) — no legacy equivalent" (NotificationsView.vue:1-6)

12.2 New / redesigned widgets and sections

Item Path Evidence
Live 18k rate strip (RateStrip) on home + dashboard src/components/home/RateStrip.vue; used src/views/HomeView.vue:44, src/views/user/DashboardView.vue:118 Replaces legacy's multi-row GET /price ticker (DashboardView.vue:28-31)
Editorial hero slider (dependency-free, pointer-drag, 6 s autoplay) src/components/home/ShowcaseSlider.vue Replaces the Swiper HomeSlider (now unused) — ShowcaseSlider.vue:1-13
Admin-managed home banner consuming GET /home/slider src/api/client.ts:168-170; src/views/HomeView.vue:155, 176-201, 257-263 New endpoint + new fallback-to-category hero logic
ShowcaseCard / ShowcaseSlider / CategoryRail / SectionHead / AccountStrip — whole components/home/ family src/components/home/* New design-system components; ShowcaseCard.vue:1-9
"برگزیده منتزر" featured section (wide hero card + 2-col grid) src/views/HomeView.vue:56-68 New home section
"تازه‌ها" horizontal reel on home src/views/HomeView.vue:70-86 New home section
Trust badges row (خرید امن / اصالت کالا / ارسال بیمه‌شده) src/views/HomeView.vue:93-106; also on product detail src/components/product/SingleProduct.vue:193-196 New widget
PageBar — shared translucent sticky bar replacing the legacy dark AppHeader src/components/ui/PageBar.vue:1-9 Whole-app chrome redesign; AppHeader deleted (src/router/index.ts:74-79)
Redesigned 5-tab bottom nav with CSS sliding indicator (replaces 4-tab GSAP SVG notch) src/components/ui/Navigation.vue:1-10; src/stores/navStore.ts:25-31 Explicit "Redesigned bottom nav — replaces the old GSAP SVG-notch bar"
GallerySlider — Pointer-Events gallery rewrite with LTR-pinned track + full-screen teleported preview with thumbnails src/components/product/GallerySlider.vue:1-47; preview src/components/product/SingleProduct.vue:239-274 From-scratch rewrite, not a port
Variant chips with per-variant images + stock badges + "نمایش همه" collapse (VARIANT_PREVIEW_COUNT = 6) src/components/product/SingleProduct.vue:105-154, 370-374, 383-393 New variant-picker design
Stock / pre-order model in UI (qty, isPreOrder, isVariantOrderable, cart capQty/maxQty clamping, disabled +) src/composables/useProduct.ts:25-27,57-62; src/stores/cartStore.ts:56-93; src/components/product/CartItem.vue:82-88; src/components/product/SingleProduct.vue:466-480 New business rule not in a 2023 reference
Pull-to-refresh (vant PullRefresh) on Home, Cart, Search, Orders, Dashboard HomeView.vue:38-42; CartView.vue:35-39; SearchView.vue:65-69; OrdersView.vue:51; DashboardView.vue:51 New interaction
IntersectionObserver incremental rendering (PAGE_SIZE = 20) replacing server pagination src/views/search/SearchView.vue:169-204; src/views/NewestView.vue:49-71 New rendering strategy
Ledger-based "اسناد" screen (LedgerEntry + CREDIT/DEBIT + confirmed state + detail drawer) src/views/user/TransactionsView.vue:1-19, 112-140 Replaces legacy Tahesab/Cash documents
Order detail as bottom Drawer with 4-step tracker + OrderItemCard lookup join src/views/user/OrdersView.vue:67-141; src/composables/useOrderItemProduct.ts Redesigned flow (was a full-screen ant Modal)
Order status chips using the new OrderStatus enum (9 chips) src/views/user/OrdersView.vue:167-177; src/utils/orderStatus.ts:8-17 New enum, new labels
Dotlottie "new version" update dialog src/App.vue:22-39, 46 (@lottiefiles/dotlottie-vue, remote lottie.host asset) New dependency + new UX
Balance card with بدهکار/بستانکار seal on dashboard src/views/user/DashboardView.vue:55-78, 176-180 Redesigned; also duplicated in unused AccountStrip.vue
Brand-logo Spinner indicator src/components/Spinner.vue:5 New brand chrome

12.3 New/changed data contracts worth protecting

Item Path
GET /home/slider + AdminSlide type src/api/client.ts:152-157, 168-170
ProductVariantSummary.qty / isPreOrder / imageId / image src/api/client.ts:29-40
ProductSummary.productCode / collections[] src/api/client.ts:65-74
GoldPriceQuote (karatPerMille, pricePerGramRial, quotedAt) src/api/client.ts:144-150
session localStorage schema gold-bank:* alongside legacy appToken src/stores/session.ts:11-32

12.4 Deliberate regressions / stubs (documented in-file — verify before "fixing")

Item Path
OTP code is not verified; any 4 digits log in as the matched customer src/views/user/LoginView.vue:152-160
Registration posts nothing (no create-customer endpoint) src/views/user/RegisterView.vue:117-125
Favourites are in-memory only, no backend src/stores/favoritesStore.ts:1-5
Placeholder home slides in configStore (used only if HomeSlider were mounted — it isn't) src/stores/configStore.ts:39-58
SearchInput clear-icon truthiness bug preserved src/components/search/SearchInput.vue:8-11
HomeSlider query.discount vs has_discount bug preserved src/components/slider/HomeSlider.vue:85-90
Slider.vue showPopup index bug preserved src/components/slider/Slider.vue:2-9
MasterLayout bypasses App.vue's keep-alive/suspense src/layout/MasterLayout.vue:3-7
DialogVant / DialogVantOneButton rely on attribute fallthrough for show src/components/DialogVant.vue:2-9

12.5 Dead code (no importers) — candidates for removal, not parity gaps

src/components/home/AccountStrip.vue, src/components/product/Category.vue, src/components/slider/HomeSlider.vue, src/components/slider/Slider.vue, src/components/ui/ContentWrapper.vue, src/components/ui/ScrollFetch.vue, src/components/ui/ScrollFetchAfterSearch.vue, src/components/ViewAll.vue, src/stores/loadingStore.ts, api.getBalance (src/api/client.ts:163), gsap dependency (package.json:15).


Summary counts

Metric Count
Named routes 13 (+2 redirects)
View files under src/views 14
API functions 9 (1 uncalled)
Pinia stores 9 (1 unused)
Reusable components 34 .vue under src/components (8 with no importers)
Composables 5
Util modules 4
Persisted localStorage keys 5