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>
51 KiB
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)./ordersis gated by the router'srequiresAuthand bounces to/login. - Visibility: bar is
v-show="nav.showNav"(Navigation.vue:12).router.afterEachsetsshowNav=true+setActiveLink(index)only for the five tab routes, elsefalse(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 inonBeforeRouteLeave(:262-264). - Other nav surfaces: home bar cart/search links
src/views/HomeView.vue:25-34; dashboard 4-tile gridsrc/views/user/DashboardView.vue:184-200;AccountStrip3 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 |