From 71192ba2baf7f9148fa381cf1b189cc41f809240 Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Mon, 21 Sep 2026 18:09:46 +0330 Subject: [PATCH] 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 --- docs/parity/00-INVENTORY.md | 271 +++++ docs/parity/CHANGELOG.md | 29 + docs/parity/ISSUES.md | 76 ++ docs/parity/PROGRESS.md | 75 ++ docs/parity/PROTECTED.md | 193 ++++ docs/parity/_inv-ref-admin.md | 1705 +++++++++++++++++++++++++++++ docs/parity/_inv-ref-pwa.md | 1178 ++++++++++++++++++++ docs/parity/_inv-ref-server.md | 1313 ++++++++++++++++++++++ docs/parity/_inv-tgt-admin.md | 1261 +++++++++++++++++++++ docs/parity/_inv-tgt-api.md | 1179 ++++++++++++++++++++ docs/parity/_inv-tgt-userapp.md | 612 +++++++++++ docs/parity/_inv-tgt-userappv2.md | 700 ++++++++++++ 12 files changed, 8592 insertions(+) create mode 100644 docs/parity/00-INVENTORY.md create mode 100644 docs/parity/CHANGELOG.md create mode 100644 docs/parity/ISSUES.md create mode 100644 docs/parity/PROGRESS.md create mode 100644 docs/parity/PROTECTED.md create mode 100644 docs/parity/_inv-ref-admin.md create mode 100644 docs/parity/_inv-ref-pwa.md create mode 100644 docs/parity/_inv-ref-server.md create mode 100644 docs/parity/_inv-tgt-admin.md create mode 100644 docs/parity/_inv-tgt-api.md create mode 100644 docs/parity/_inv-tgt-userapp.md create mode 100644 docs/parity/_inv-tgt-userappv2.md diff --git a/docs/parity/00-INVENTORY.md b/docs/parity/00-INVENTORY.md new file mode 100644 index 0000000..03ac753 --- /dev/null +++ b/docs/parity/00-INVENTORY.md @@ -0,0 +1,271 @@ +# 00 — INVENTORY & MAPPING (Phase 0) + +Read-only inventory of all four reference/target pairs, plus the mapping tables +that drive Phase 1 and Phase 2. + +**Method:** seven parallel read-only subagents, one per project, each citing +`path:line`. Their raw output lives in the `_inv-*.md` files next to this one; +this document is the consolidated map. Claims marked ✔ below were additionally +re-verified by the main session by reading the cited lines directly. + +| Detail inventory | Project | Lines | +|---|---|---| +| `_inv-ref-server.md` | `henza_server` (reference backend) | 1313 | +| `_inv-tgt-api.md` | `apps/api` (target backend) | 1179 | +| `_inv-ref-admin.md` | `henza_admin` (reference admin) | 1705 | +| `_inv-tgt-admin.md` | `apps/admin-panel` (target admin) | 1261 | +| `_inv-ref-pwa.md` | `henza_pwa` (reference user app) | 1178 | +| `_inv-tgt-userapp.md` | `apps/user-app` (target v1) | 612 | +| `_inv-tgt-userappv2.md` | `apps/user-app-v2` (target v2) | 700 | + +--- + +## 0. The single most important finding — the domain is NOT gold banking + +The kickoff brief asks to audit "gold price, gram/weight, fees, commissions, +spread, wallet/balance, buy/sell orders, deposit/withdraw, transfers, KYC levels, +daily limits, payment callbacks, live price sockets". **Most of that does not +exist in the reference**, so it cannot be a parity gap. + +`henza_server` + `henza_pwa` are a **gold-jewellery B2B wholesale + B2C +e-commerce** system (Henza Gold): a catalogue of physical SKUs priced from a gold +rate, a cart, orders, and a gram-denominated customer ledger. + +Verified absences in the reference user app (`_inv-ref-pwa.md` §6.2): + +| Brief's flow | Reality in `henza_pwa` | +|---|---| +| buy gold | ABSENT — nearest is add-to-cart for a physical jewellery SKU | +| sell gold | ABSENT | +| wallet | ABSENT — a read-only balance strip on the dashboard | +| deposit | ABSENT — nearest is submitting a *findoc* of type چک/حواله/آب‌شده | +| withdraw | ABSENT | +| transfer | ABSENT — findoc type 2 (`حواله`) only records a transfer note | +| KYC levels | PARTIAL — two flat flags, no levels | +| addresses | ABSENT — one free-text field on the user | +| tickets / support | ABSENT — a static WhatsApp FAB (`index.html:59-61`) | +| daily/min/max limits | only one exists: `gram_level` (`ApiMobileController.php:528-758`) | +| live price socket | ABSENT — `BROADCAST_DRIVER=null`, zero `ShouldBroadcast` events ✔ | +| payment callback | route `site/transaction/verify` is **not registered** anywhere | +| cron | `Console/Kernel::schedule()` is **empty** | + +**Consequence for the plan:** the parity matrices will cover what the reference +actually has — catalogue, pricing formula, cart/orders, ledger ("اسناد"), customer +management, ACL — and will not manufacture rows for gold-banking features. Where +the *target* has something bank-like, it is an intentional addition, not a gap. + +--- + +## 1. Stack mapping + +| Layer | Reference | Target | Note | +|---|---|---|---| +| Backend | Laravel/PHP, 46 controllers, 57 models | NestJS 10 + Prisma 5, 25 modules, 29 models | total rewrite; port behaviour only | +| Admin | Vue 2 + Vuesax/Vuexy template, Vuex | Vue 3.5 + ant-design-vue 4.2.6, Pinia | total rewrite | +| User app | Vue 3 + Vite + vant + ant-design-vue 3 | Vue 3 + Vite, two design versions | closest pair | +| Money types | PHP floats throughout | `Prisma.Decimal`, **zero `Float` in schema** ✔ | target is *stricter* — keep it | + +--- + +## 2. Backend mapping — `henza_server` ⇄ `apps/api` + +### 2.1 Scale + +| Metric | `henza_server` | `apps/api` | +|---|---|---| +| Routes / endpoints | **369** (admin 215, api 70, customer 44, site 30, web 10) | **179** | +| Controllers | 46 | 25 | +| Models | 57 files (34 migrated tables + ~28 legacy Django-era tables) | 29 Prisma models | +| Enums | PHP static arrays | 14 Prisma enums | +| DTO / validation | **zero FormRequests**, all inline, several endpoints unvalidated | 8 DTO classes; most admin bodies untyped `Record` | +| Seeders | 1, **empty** | `prisma/seed.ts` — user types, 144 rids, admins | + +The 369 → 179 drop is expected, not automatically a gap: the reference counts +`Route::resource` expansions, ~28 dead legacy tables, a `site.php` storefront and +a `web.php` that the monorepo replaces with its own apps. **Which of the 369 are +genuinely unmapped is Phase 1 / Phase 2 matrix work**, per route group. + +### 2.2 Core business logic — where each rule lives + +| Rule | Reference | Target | +|---|---|---| +| **Gold price formula** ✔ | `app/Models/ProductVariant.php:183-190` | `apps/api/src/modules/...` (Phase 1 to map) | +| Price source | `app/Models/WebServiceGold.php` — `getPriceGold18Ayar()` divides by 10 (rial→toman) | `pricing` module | +| Order pipeline | `app/Http/Controllers/Controller.php` — `createSingleCustomerOrders`, `afterCreateOrder`, cashback at sts 6 | `orders.service.ts`, `admin-orders.service.ts` | +| Wholesale order + the only limit | `ApiMobileController.php:528-758` (`gram_level`) | `orders.service.ts` | +| Retail checkout | `Customer/OrderController.php:68-168` | `orders.service.ts` | +| Ledger / wallet | `packages/rooyesh/wallet/` — hash chain, pepper `"yhoenszeaf"` | `accounting/ledger.service.ts` (dual-currency, `Prisma.Decimal`) | +| Cashback | `Adm/AdminController.php:985-1013` (`chargeWalletAfterOrder`, in grams) | `admin-customers.service.ts` | +| Order statuses | **two divergent enums**: `Order.php:66` (0–6) and `SingleCustomerOrders.php:67` (1–7) | `OrderStatus` + `PreOrderStatus` | +| SMS / OTP | `app/Models/Sms.php` | **absent** — DB persistence only | + +#### The gold price formula, verified ✔ + +`henza_server/app/Models/ProductVariant.php:183-190`: + +```php +$price = goldPrice + (goldPrice * ((percentage_pay + product_price_extra_percent) / 100)); +$price += $price * 0.07; // 7% margin +$temp = $price - goldPrice; // non-metal portion only +$temp += $temp * 0.09; // 9% VAT on that portion only +$price = goldPrice + $temp; +$price *= grams; +return floor($price / 1000) * 1000; // round DOWN to the nearest 1000 +``` + +Three superseded variants sit commented out above it — they apply VAT to the +whole price, which is **wrong** and must not be ported. `getPriceWithDiscountAttribute` +(`:255-280`) is the same formula with `0.07 - discount_percent/100` and a trailing +`price -= price * (discount_price/100)`. + +This formula, its ordering, and the `floor(/1000)*1000` are **row #1 of the Phase 1 +matrix** — it is the money rule the whole catalogue depends on. + +### 2.3 Auth & permissions + +| Aspect | Reference | Target | +|---|---|---| +| Admin route protection | **NONE** ✔ — `auth:api` commented at `routes/admin.php:12` and `:272`, so all ~215 admin routes are public | global `JwtAuthGuard` + `PermissionsGuard`; 15 routes opt out with `@Public()` ✔ | +| Permission model | `Permission(rid, ut)` rows returned at login (`AdminController:74`), enforced **client-side only** — no Gates/Policies/Spatie | `acl.constants.ts` — 144 rids + `expandWithParents()`, enforced **server-side** | +| Permission catalogue source | not derivable from the server; lives in the admin at `views/user/permissionConst.js` (**172 declared keys**) | `acl.constants.ts` (144 rids) + `permissionConst.ts` (**179 keys, 174 unique**) | +| Parent expansion | `store/users/index.js:3-21` `checkParent()` — `order.list.sts.remove` implies `order.list.sts`, `order.list`, `order` | `expandWithParents()` — same semantics | +| Token refresh | machinery exists but is **unreachable** (`main.js:22-23` commented) | none | + +**The target is deliberately stricter than the reference on auth.** That is a +prior, documented decision (`docs/CHANGELOG-PORT-HISTORY.md`), not a parity gap — +and reverting it would reopen a critical hole. Logged as `SEC-01`/`SEC-02`. +**This needs your confirmation (see Q1).** + +--- + +## 3. Admin mapping — `henza_admin` ⇄ `apps/admin-panel` + +| Metric | `henza_admin` | `apps/admin-panel` | +|---|---|---| +| `.vue` under `views/` | **541** — but only ~69 reachable from the live router; the rest are Vuexy template demos | **91** `.vue` + 19 `.ts` table models = 110 files | +| Routes | admin build + branch build (two route trees) | 80 admin-mode + 15 branch-mode records | +| Permission keys | 172 in `permissionConst.js` | 179 (174 unique) in `permissionConst.ts` | +| Rids enforced by `meta.role` | ~58 | ~58 | +| Button-level `$hasPermission` | catalogued in `_inv-ref-admin.md` §5.3 | 73 distinct rids via `hasPermission()` | +| HTTP | `src/axios.js`, **no response interceptor**, no working refresh | `src/api/client.ts` + `endpoints.ts` legacy→REST map (~100 exact + 34 regex), 401 → clear token + redirect | +| Table system | `src/components/x-table/` (4 forked copies) | one `XTable`, POST `{search, filters, page, limit, order, select, titles, report_title}` → `{rows, query}` | +| Digit/price utils | **none exist** (`_inv-ref-admin.md` Appendix B #17) | none, except inside `DateInput.vue` | + +**Route-tree note:** `src/router/{apps,category,customer,dashboard,full,manage,order,product,temp}.js` +exist in the reference but are **not imported** — only `admin.js` and `branch.js` +are (`src/router.js:6,8`). Nine dead route files. They must not be mistaken for +missing pages in Phase 1. + +### 3.1 Mapping status + +A route-by-route and action-by-action mapping is **Phase 1's deliverable** +(`01-ADMIN-MATRIX.md`). What Phase 0 establishes is the denominator: ~69 live +reference views against 91 target views, with the reference's 172-key permission +tree against the target's 174-unique-key tree. + +--- + +## 4. User-app mapping — `henza_pwa` ⇄ `user-app` + `user-app-v2` + +### 4.1 Routes + +| Reference route | name | `user-app` / `user-app-v2` | Status | +|---|---|---|---| +| `/home` | home | `/home` → `HomeView.vue` | ✅ | +| `/collection/:id?` | collection | same | ✅ | +| `/search/:id?` | search | `/search` — **no `:id?` param** | ⚠️ different | +| `/product/:id` | singleProduct | `components/product/SingleProduct.vue` | ✅ | +| `/newest` | newestProduct | same | ✅ | +| `/user` | userPage::hasSubRoute | `DashboardView.vue` | ✅ | +| `/orders` | ordersList | same | ✅ | +| `/transactions` | transactionsList | same | ✅ | +| `/profile` | profileData | same | ✅ | +| `/favorites` | favoritesList | same | ✅ | +| `/cart` | cart | same | ✅ | +| `/login` | login | `LoginRegisterView.vue` | ✅ | +| **`/report`** | report | — | ❌ missing | +| **`/report/detail`** | reportDetail | — | ❌ missing | +| **`/ChildOrder`** | ChildOrder (sales-rep child orders) | — | ❌ missing | +| **`/customerChoice`** | customerChoice (impersonation) | — | ❌ missing | +| **`/complete_profile`** | completeProfile (document upload / KYC) | — | ❌ missing | +| — | — | **`/notifications`** | ➕ extra in target | + +Verified by reading `henza_pwa/src/router/index.ts:98-215` against +`apps/user-app/src/router/index.ts:18-45` ✔. The two target routers are +**byte-identical** (`diff` returns nothing) ✔ — good for the v1/v2 parity rule. + +### 4.2 Guard differences ✔ + +| Behaviour | `henza_pwa/src/router/index.ts` | target | +|---|---|---| +| unauth hitting a gated route | `showNotify('ابتدا وارد شوید')` then redirect to **`home`** (`:250-254`) | redirect to **`/login`** with `?redirect=`, no toast | +| gated route list | ordersList, transactionsList, profileData, favoritesList, **completeProfile** | `meta.requiresAuth` on 4 (no completeProfile — the route doesn't exist) | +| logged-in hitting `/login` | → home (`:256`) | → home ✅ | +| `!isLoaded && to.name !== 'home'` | → home (`:258`) | **absent** | +| `sts_verify_step_two ∈ {1,2}` blocks completeProfile | `:260` | **absent** | +| route analytics | `useCustomAnalytics()` → `POST /log/create/{key}`, `GET /log/update/{id}` (`customAnalytics.ts:25,32`) | **absent entirely** | + +### 4.3 The v1 ⇄ v2 rule + +`CLAUDE.md` §2: the only allowed difference is visual design. Five **logic** +differences found, all with v2 behind v1 — these are parity gaps *inside* the +target, independent of the reference: + +| # | Gap in `user-app-v2` | Evidence | +|---|---|---| +| 1 | **No cart stock ceiling** — no `capQty`/`maxQty`; a user can order more units than exist | v1 `stores/cartStore.ts:62,65`, `components/product/CartItem.vue:82-83`; zero hits in v2 ✔ | +| 2 | No `getHomeSlider()` / `GET /home/slider` admin banner | `diff` of the two `api/client.ts` ✔ | +| 3 | No per-variant images — v1 has `ProductImageSummary{id,url,sortOrder,role}` + `imageId`/`image`; v2's `images` is `Array<{url}>` | same diff ✔ | +| 4 | No orderable-first variant defaulting | v1 `composables/useProduct.ts:26` | +| 5 | No variant-chip preview cap / per-chip stock badges | v1 `components/product/SingleProduct.vue:388-392` | + +Logged as `MON-01` and `V2-01..05` in `ISSUES.md`; they become Phase 2 matrix rows. + +### 4.4 Live price — reference vs target ✔ + +Neither side has a socket. But the reference has a **two-path bug** worth knowing +before porting: `baseStore.getPriceList()` (`store.ts:107-156`) and +`Dashboard.getListPrice()` (`Dashboard.vue:632-660`) both do a one-shot +`GET /price`; the visible grid comes from the second, while the manual refresh +button re-runs the first — **so the refresh button does not update the list the +user is looking at**. Also `prices.date`/`prices.time` assignments are commented +out (`store.ts:145-147`), so the dashboard's date/time header is permanently blank. + +Target: one-shot `GET /pricing/latest?karatPerMille=750` on mount + pull-to-refresh, +in `HomeView` and `DashboardView` only. + +--- + +## 5. What Phase 0 did NOT determine (goes to Phase 1 / Phase 2) + +- Route-by-route mapping of the reference's 369 backend routes onto the target's 179. +- Field-by-field comparison of request/response shapes per endpoint. +- Per-view form/validation/table comparison for the ~69 live admin views. +- Whether the target's gold-price implementation matches `ProductVariant.php:183-190` + exactly, including the `floor(/1000)*1000`. **This is Phase 1 row #1.** +- Whether the 174 target permission keys are a superset/subset of the reference's 172. + +--- + +## 6. Open questions — need your answer before Phase 1 + +See the gate message; recorded here so they survive a context reset. + +- **Q1 — Auth on admin routes.** The reference leaves all ~215 admin routes + unauthenticated (`routes/admin.php:12` commented). The target enforces JWT + + per-rid permissions. Strict parity would mean *removing* auth. Confirm the + target's stricter behaviour stands as an accepted deviation. +- **Q2 — Scope of the "gold banking" checklist.** Confirm we audit the domain the + reference actually implements (§0) rather than manufacturing rows for + buy/sell/deposit/withdraw/KYC-levels/sockets that exist nowhere. +- **Q3 — The 5 missing user-app routes** (`/report`, `/report/detail`, + `/ChildOrder`, `/customerChoice`, `/complete_profile`). These are real reference + features absent from both target apps. Building them means ~5 screens × 2 designs + plus backend. In scope for Phase 2, or deferred? +- **Q4 — Reference bugs.** 20 defects found in `henza_pwa` and 18 in `henza_admin` + (listed in the `_inv-*` appendices). `CLAUDE.md` §3 says replicate + log. Confirm + we do **not** replicate the data-destroying / clearly-broken ones (e.g. the + refresh button that updates an invisible list), and log them instead. +- **Q5 — Stray folder** `gold-banking-monorepo/user-app-v2/` (repo root, outside + `apps/`): only `package.json` + an outdated `vite.config.ts`, ignored by + pnpm-workspace. Delete, or leave? diff --git a/docs/parity/CHANGELOG.md b/docs/parity/CHANGELOG.md new file mode 100644 index 0000000..46d3e7e --- /dev/null +++ b/docs/parity/CHANGELOG.md @@ -0,0 +1,29 @@ +# CHANGELOG — parity changes + +Every change made to `gold-banking-monorepo` during the parity project, with the +reason and the reference path that justifies it. + +Format: + +``` +## — — +- **What:** +- **Why:** +- **Reference:** +- **Target:** +- **Matrix row:** +- **Commit:** +``` + +--- + +## 2026-09-21 — Phase 0 — Setup + +- **What:** Created branch `parity/phase-0` and `docs/parity/` scaffolding + (`PROGRESS.md`, `ISSUES.md`, `CHANGELOG.md`). +- **Why:** Persistent state required by `CLAUDE.md` §6. +- **Reference:** n/a +- **Target:** `docs/parity/` +- **Matrix row:** n/a + +_No source code changed in Phase 0 (read-only phase)._ diff --git a/docs/parity/ISSUES.md b/docs/parity/ISSUES.md new file mode 100644 index 0000000..0f761f9 --- /dev/null +++ b/docs/parity/ISSUES.md @@ -0,0 +1,76 @@ +# ISSUES — logged, NOT fixed during parity + +Anything found during the parity audit that is a bug, risk, smell or improvement +opportunity goes here. **Do not fix these while parity work is in progress** (see +`CLAUDE.md` §3). They are worked through in Phase 3, only when the user asks. + +Severity: `Critical` (money loss / security / data loss) · `High` · `Medium` · `Low` + +> Paths are relative to the workspace root (`gold bank/`). +> "REF" = reference project (read-only). "TGT" = `gold-banking-monorepo`. +> Items marked **[replicated]** exist in the reference and were faithfully ported — +> fixing them is a product decision, not a parity fix. + +## Security & auth + +| ID | Severity | Where | Description | Evidence | Suggested fix | +|---|---|---|---|---|---| +| SEC-01 | Critical | REF `henza_server/routes/admin.php:12,272` | The whole admin route file is **unauthenticated** — the `Route::middleware('auth:api')->group(...)` wrapper is commented out at both ends, leaving all ~215 admin routes public (incl. `wallet/charge`, `users`, `roles`). | Verified by reading both lines. | n/a in reference (read-only). Drives TGT decision below. | +| SEC-02 | Critical | TGT `apps/api/src/modules/accounting/accounting.controller.ts:7`, `orders/orders.controller.ts:7`, `customers/customers.controller.ts:5`, `catalog/catalog.controller.ts:5`, `pricing/pricing.controller.ts:5` | 15 routes carry `@Public()`, including **ledger writes** (`POST accounting/repayments/*`, `entries/:id/confirm`) and `POST /api/orders` + `PATCH /api/orders/:id/status`. The public status DTO accepts a client-supplied `actorAdminUserId`, so a caller can attribute an action to any admin. | `grep -rn "@Public()"`; source comments say "pre-existing unauthenticated endpoint". | Introduce customer-session auth for the app-facing routes; move ledger/status writes behind the admin guard. **[replicated]** from SEC-01. | +| SEC-03 | Critical | TGT `apps/user-app/src/views/user/LoginView.vue`, `apps/user-app-v2/` same | OTP is **not verified** — any 4-digit code logs the user in. Login also downloads the **entire customer list** (`GET /api/customers`) and matches the phone number client-side, exposing every customer's data to any visitor. | Both inventory agents verified independently; `customers.controller.ts:5` is `@Public()`. | Implement a real server-side OTP issue/verify pair; never ship the customer list to the client. | +| SEC-04 | High | TGT `apps/api` (two files) | `JWT_SECRET` falls back to `'dev-only-insecure-secret'` and is absent from `.env.example`. `enableCors()` is wide open. | Reported by api inventory; see `_inv-tgt-api.md` §11. | Require the env var at boot; restrict CORS origins. | +| SEC-05 | High | TGT `apps/api/prisma/seed.ts` | Seed creates a SUPER_ADMIN (`09128081392`) whose **password equals the phone number**, holding all 144 rids. Marked DEV-ONLY in comments but lives in the normal seed. | `docs/CHANGELOG-PORT-HISTORY.md` §4 documents this as intentional for dev. | Gate behind an explicit env flag so it can never run outside dev. | +| SEC-06 | High | REF `henza_server` | Hardcoded secrets committed to the repo: SMS panel password, PayPing bearer token, Tahesab bearer + IP allowlist, wallet hash pepper (`"yhoenszeaf"`). | See `_inv-ref-server.md` §11.4. | Rotate all of them before any deployment that reuses these credentials. | +| SEC-07 | Medium | TGT `apps/api/src/modules/admin-customers/admin-customers.service.ts` | `setRoles` stores arbitrary strings as rids with no validation against `ALL_RIDS`. | api inventory §9. | Validate against the rid catalogue. | + +## Money, stock & data integrity + +| ID | Severity | Where | Description | Evidence | Suggested fix | +|---|---|---|---|---|---| +| MON-01 | Critical | TGT `apps/user-app-v2/src/stores/cartStore.ts` | v2 has **no `capQty` / `maxQty` stock ceiling**, which `user-app` does have (`apps/user-app/src/stores/cartStore.ts:62,65`; `components/product/CartItem.vue:82-83`). A v2 user can order more units than exist. | Verified by grep across both apps — zero hits in v2. | Port the ceiling to v2 (this is also a **Phase 2 parity gap**, row to be added to `02-APPS-MATRIX.md`). | +| MON-02 | High | REF `henza_server` wallet package | The wallet `balance` column **mixes grams and toman in one field**, and money operations are not wrapped in DB transactions. | `_inv-ref-server.md` §7. | Separate the units; wrap mutations in transactions. | +| MON-03 | High | TGT `apps/api/src/modules/accounting/ledger.service.ts:189` | `GRAMS_PER_MESGHAL = 4.331802` is a JS **float** used inside an otherwise-`Prisma.Decimal` CASH repayment formula, so the divisor is pre-rounded before the Decimal division. | Read at `ledger.service.ts:182-190`; the source comment already flags the formula for stakeholder review. | Make the constant a `Decimal` (or a string literal passed to `Decimal`). | +| MON-04 | High | TGT `apps/api/src/modules/orders/orders.service.ts` | Debt is recognised at `DELIVERED_TO_AGENT`, but the idempotency guard checks `deliveredAt`, which is only set by `DELIVERED_TO_CUSTOMER` — so the debt trigger is not reliably idempotent. There is also **no status-transition validation** anywhere. | api inventory §8. | Guard on the same field the trigger fires on; add an explicit transition table. | +| MON-05 | Medium | TGT `apps/api` | `canPlaceOrders` and `verificationStep` are stored and toggled by the admin panel but **never enforced** on any order path. | api inventory §8. | Enforce at order creation, or remove the toggles. | +| MON-06 | Medium | TGT `apps/api` | `Setting` rows are never seeded — `wallet.config`, `wallet.birthday`, `promotion.auto`, `mobile_main_slider` only materialise on first write, so a fresh install has undefined behaviour until an admin saves each one. | api inventory §7. | Seed the defaults. | +| MON-07 | Medium | REF `henza_server/app/Models/ProductVariant.php:183-190` vs `SingleCustomerOrders.php:67`, `Order.php:66` | Two **divergent order-status enums** coexist (0–6 and 1–7) for what is conceptually one flow. | Read both models. | Unify, or document the mapping explicitly. | + +## Validation & contracts + +| ID | Severity | Where | Description | Evidence | Suggested fix | +|---|---|---|---|---|---| +| VAL-01 | High | REF `henza_server` | **Zero FormRequests**; all validation is inline, and `setOrder`, wallet `charge`, `setConst` and file upload have **no validation at all**. The wallet package's own validator is dead code (it calls `failed()` instead of `fails()`). | `_inv-ref-server.md` §12. | n/a in reference; informs how much validation the target must add. | +| VAL-02 | High | TGT `apps/api` | Only **8 DTO classes** exist; most admin request bodies are untyped `Record`, so class-validator never runs on them. | api inventory §5. | Add DTOs for the admin write paths. | +| VAL-03 | Medium | TGT both user apps | HTTP client is a bare `fetch` wrapper: **no interceptors, no Authorization header, no refresh, no 401 handling, no error toasts**. Non-2xx throws a generic `Error`; call sites mostly `console.log(e)`. Order-submit failure is **silent to the user**. | `apps/user-app/src/api/client.ts`, `apps/user-app-v2/src/api/client.ts:3`. | Add a shared client with error surfacing; compare against `henza_pwa/src/service/*` in Phase 2. | + +## Parity-relevant divergence between user-app and user-app-v2 + +> `CLAUDE.md` §2: the only allowed difference is visual design. These are logic +> differences, so they are **Phase 2 parity gaps** as well as issues. + +| ID | Severity | Where | Description | Evidence | +|---|---|---|---|---| +| V2-01 | Critical | v2 missing stock ceiling | see MON-01 | grep | +| V2-02 | Medium | v2 missing `getHomeSlider()` / `GET /home/slider` admin banner | v1 `api/client.ts:152-170` defines `AdminSlide` + `getHomeSlider`; v2 has neither | `diff apps/user-app/src/api/client.ts apps/user-app-v2/src/api/client.ts` | +| V2-03 | Medium | v2 missing per-variant images | v1 defines `ProductImageSummary` (`id/url/sortOrder/role`) and `imageId`/`image` on the variant; v2's `images` is just `Array<{url:string}>` | same diff | +| V2-04 | Medium | v2 missing orderable-first variant defaulting | v1 `composables/useProduct.ts:26` picks the first orderable variant; v2 has no equivalent | grep | +| V2-05 | Low | v2 missing variant-chip preview cap + per-chip stock badges | v1 `components/product/SingleProduct.vue:388-392` | grep | + +## Dead code & maintenance + +| ID | Severity | Where | Description | +|---|---|---|---| +| DEAD-01 | Low | TGT `apps/user-app` | 8 components with no importers, `loadingStore`, `api.getBalance` (zero call sites), unused `gsap` dependency. | +| DEAD-02 | Low | TGT `apps/user-app-v2` | 11 dead components, `loadingStore`, `getBalance`, `priceUnit`/`priceUnitRialLess`, 12 of 15 validators, unused `gsap` + `@ant-design/icons-vue` deps. | +| DEAD-03 | Low | TGT `gold-banking-monorepo/user-app-v2/` (repo root, outside `apps/`) | Stray leftover folder containing only `package.json` and an **older** `vite.config.ts` (hardcoded port 5179, hardcoded proxy, imports a `themeConfig` that doesn't exist there). Not matched by `pnpm-workspace.yaml`, so pnpm ignores it. Left in place — deleting it needs user approval. | +| DEAD-04 | Low | REF `henza_server/app/Console/Kernel.php` | `schedule()` is **empty** — no cron. Price refresh / expiry / birthday logic must be driven by OS cron or the `GET /` web route. | +| DEAD-05 | Low | REF `henza_server` | PayPing `returnUrl` points at `site/transaction/verify`, which is **not registered in any route file** — the payment callback is unreachable. | + +## Known stubs in the target (not bugs, but not parity either) + +| ID | Where | Description | +|---|---|---| +| STUB-01 | TGT both user apps | Registration persists nothing; favourites are in-memory only (lost on reload); notifications is a static empty state; home slides are 3 hardcoded placeholders in v2. | +| STUB-02 | TGT `apps/api` | No OTP, no SMS provider, no payment gateway/callbacks, no cron, no websockets. SMS and notifications are DB-persistence only. | +| STUB-03 | TGT both user apps | All list filtering and "pagination" is **client-side** over one flat `GET /catalog/products`; no server `page`/`limit` params. "Pagination" is a 20-item IntersectionObserver window. | +| STUB-04 | TGT both user apps | Live price is **pull-on-mount only** (`GET /pricing/latest?karatPerMille=750`) — no socket, no SSE, no polling interval. `quotedAt` renders as a Jalali **date only**, so a stale rate looks current. | diff --git a/docs/parity/PROGRESS.md b/docs/parity/PROGRESS.md new file mode 100644 index 0000000..3fbc7d2 --- /dev/null +++ b/docs/parity/PROGRESS.md @@ -0,0 +1,75 @@ +# PROGRESS — Reference Parity Project + +> Read this file FIRST at the start of every session and after every `/compact`. +> Rules live in `CLAUDE.md` (workspace root). Method lives in the `reference-parity-audit` skill. + +## Current state + +| Field | Value | +|---|---| +| **Phase** | Phase 0 — Inventory (read-only) | +| **Branch** | `parity/phase-0` | +| **Status** | ✅ COMPLETE — **at the gate, waiting for user confirmation** | +| **Next step** | User answers Q1–Q5 below → finalise `PROTECTED.md` → branch `parity/phase-1-admin` → build `01-ADMIN-MATRIX.md` | +| **Blocked on** | user answers to Q1–Q5 | +| **Last updated** | 2026-09-21 | + +## Phase checklist + +- [x] P0.1 — All 3 reference folders readable (`henza_server`, `henza_admin`, `henza_pwa`) +- [x] P0.2 — Branch `parity/phase-0` + `docs/parity/` created +- [x] P0.3 — Inventory all 7 projects → `_inv-*.md` (8111 lines total) → consolidated in `00-INVENTORY.md` +- [x] P0.4 — Mapping tables, missing list, extra list (`00-INVENTORY.md` §2–4) +- [x] P0.5 — Draft `PROTECTED.md` +- [ ] **P0.6 — GATE: user confirms PROTECTED list + answers Q1–Q5** ← HERE +- [ ] Phase 1 — Admin parity (branch `parity/phase-1-admin`, `01-ADMIN-MATRIX.md`) +- [ ] **GATE:** "Admin parity is complete and tested. Shall we move on to the apps (user-app & user-app-v2)?" +- [ ] Phase 2 — Apps parity (`02-APPS-MATRIX.md`, both user apps) +- [ ] Phase 3 — Work through `ISSUES.md` (only when user asks) + +## Open questions blocking Phase 1 + +| # | Question | Proposed answer | +|---|---|---| +| Q1 | Reference leaves all ~215 admin routes unauthenticated (`henza_server/routes/admin.php:12,272`). Target enforces JWT + 144 rids. Strict parity = remove auth. | Keep target's stricter auth as an accepted deviation (`PROTECTED.md` §D) | +| Q2 | The brief's gold-banking checklist (buy/sell, deposit/withdraw, transfer, KYC levels, sockets, payment callbacks) does not exist in the reference — it's a jewellery e-commerce system. | Audit the domain the reference actually has (`00-INVENTORY.md` §0) | +| Q3 | 5 reference user-app routes missing from BOTH target apps: `/report`, `/report/detail`, `/ChildOrder`, `/customerChoice`, `/complete_profile`. ~5 screens × 2 designs + backend. | In scope for Phase 2, or deferred? | +| Q4 | 20 defects in `henza_pwa` + 18 in `henza_admin`. Replicate or log? | Log, don't replicate the broken/destructive ones (`PROTECTED.md` §F) | +| Q5 | Stray `gold-banking-monorepo/user-app-v2/` at repo root (only `package.json` + outdated `vite.config.ts`, ignored by pnpm-workspace). | Delete, or leave? | + +## Key context (survives context loss) + +- **Domain correction:** the reference is a **gold-jewellery B2B/B2C e-commerce** + system, not a gold bank. No buy/sell order book, no spread, no deposit/withdraw, + no P2P transfer, no KYC levels, no daily limits (only `gram_level`), no payment + callback route, no cron, no websockets. See `00-INVENTORY.md` §0. +- **The gold price formula** (Phase 1 row #1) — + `henza_server/app/Models/ProductVariant.php:183-190`: `+extra%` → `+7%` margin → + `+9%` VAT **on the non-metal portion only** → `×grams` → `floor(/1000)*1000`. + Three superseded commented-out variants above it apply VAT to the whole price — + do **not** port those. +- **A large prior port already happened** — see `docs/ADMIN-PORT-PLAN.md` and + `docs/CHANGELOG-PORT-HISTORY.md` (80 views, 25 Nest modules, 29 Prisma models, + 154 admin routes, 144/144 rids). Prior port notes **no view was ever visually + verified in a browser**. +- **Stack changed across the port:** Laravel→NestJS/Prisma, Vue2/Vuesax→Vue3/antd. + Port behaviour, not code style. +- **Reference scale is inflated:** 541 admin `.vue` files but only ~69 reachable + (rest are Vuexy template demos); 9 reference router files are dead (not imported); + ~28 legacy Django-era DB tables have no migrations. +- **v1 ⇄ v2 logic gaps** (5, all v2 behind v1) are parity work, not design: + `ISSUES.md` MON-01, V2-01..05. + +## Inventory files (Phase 0 working files) + +| File | Scope | Lines | +|---|---|---| +| `_inv-ref-server.md` | henza_server | 1313 | +| `_inv-tgt-api.md` | apps/api | 1179 | +| `_inv-ref-admin.md` | henza_admin | 1705 | +| `_inv-tgt-admin.md` | apps/admin-panel | 1261 | +| `_inv-ref-pwa.md` | henza_pwa | 1178 | +| `_inv-tgt-userapp.md` | apps/user-app | 612 | +| `_inv-tgt-userappv2.md` | apps/user-app-v2 | 700 | + +Consolidated into `00-INVENTORY.md`. Issues found: `ISSUES.md` (26 entries). diff --git a/docs/parity/PROTECTED.md b/docs/parity/PROTECTED.md new file mode 100644 index 0000000..ca1be2e --- /dev/null +++ b/docs/parity/PROTECTED.md @@ -0,0 +1,193 @@ +# PROTECTED — intentional monorepo additions (additive-only) + +**DRAFT — awaiting user confirmation (Phase 0 gate).** + +Items here are intentional changes in `gold-banking-monorepo`. Per `CLAUDE.md` §4 +they must **not** be removed, restyled, restructured, or have their behaviour +changed. You may only **ADD** what the reference has and they lack, in a way that +doesn't break them. Any fix that touches a protected item needs explicit approval +with a diff plan first. + +Legend: **[brief]** = named as protected in the kickoff brief · **[found]** = +discovered in Phase 0, proposed for protection · **[?]** = needs your ruling. + +--- + +## A. Admin panel — Dashboard · **[brief]** + +**Location:** `apps/admin-panel/src/views/Dashboard.vue` (327 lines) — one file +serving both `/dashboard/ecommerce` and `/dashboard/analytics`. +**Gate:** rid `dashboard.view`; lacking it redirects to `my-data` (not a 403 page). +**Data:** a single `GET admin/dashboard/overview` on mount, then client-side sorts +(`reagent` desc by `grams_sell`; `lastOrder` desc by `w`). + +Contents, in render order: + +1. 4 KPI tiles — مشتریان جدید (نفر) · طلای فروش‌رفته (گرم) · تعداد سفارشات · نرخ هر گرم طلای ۱۸ عیار +2. 2 KPI tiles — سفارشات تحویل‌شده · مجموع بدهی طلای مشتریان (گرم) +3. Area chart «روند سفارش‌ها» — 2 series (تک / عمده); x-labels rotated **-45°** with + `rotateAlways` + `hideOverlappingLabels` (a deliberate overlap fix — do not "tidy" it) +4. 2 donuts — وضعیت سفارشات (8-entry colour array) · تفکیک عمده/تک +5. Horizontal bar + an always-rendered agent table (کارشناس فروش / میزان فروش گرم / تعداد سفارش) +6. Vertical bar «محصولات پرفروش» with an `a-empty` fallback +7. `a-descriptions` «میزان فروش» — 3 columns incl. a computed average +8. Table «سفارشات هفته جاری» — `pageSize` 10, `sts_str` as a coloured `a-tag`, `caj` via `jdate` + +Fixed palette `#1c2d56 / #28c76f / #ff9f43 / #ea5455`. **No filters, no date range, +no export, no refresh** — that is the intended design, not a gap. + +> Why this matters for parity: the reference's two dashboards +> (`DashboardECommerce.vue`, `DashboardAnalytics.vue`) were found in the earlier +> port to be **mostly commented-out Vuexy demo markup**, with ApexCharts imported +> but never used in live markup (`docs/CHANGELOG-PORT-HISTORY.md`). So there is +> essentially nothing to port *into* this dashboard. Phase 1 will confirm and +> record that, and will not touch this file. + +--- + +## B. Admin panel — the extra option in product Variants · **[brief]** + +**Location:** `apps/admin-panel/src/views/apps/product/AddProduct.vue:151-270, +315-361, 519-568` (Create/Edit Product). +**Gate:** the card is gated `product.list.variants`; add/edit/remove on +`.add` / `.edit` / `.remove`. + +Variant table columns: تصویر | عنوان | وزن | اجرت درصدی | تعداد | فعال (tag) | +« پیش سفارش» (tag) | عملیات. + +**The protected extra field is `تصویر تنوع` (variant image).** It is a **picker, +not an uploader**: a 64×64 flex grid over the product's already-saved gallery +(`data.gallery`, each `{id, url, role, sortOrder}`), whose first tile is a +`بدون تصویر` option and whose selected tile gets a blue `.is-on` border. +`pickVariantImage()` sets `image_id` (posted) plus `image_url` (thumbnail only, +stripped before POST). + +It is populated **only in edit mode** — on create the gallery is empty and an +`a-alert` shows a context-dependent message (`ابتدا محصول را ذخیره کنید…` vs +`برای این محصول تصویری ثبت نشده است.`). + +Variants are edited **purely in memory** (row key `__key = v--`) and +saved with the product via `PUT product/update/{id}` / `POST product/create` under +a `variants[]` array — there is **no per-variant endpoint**. Only validation is +`عنوان تنوع الزامی است`; delete has **no confirm dialog**. + +> Related, and NOT protected by itself: the same `imageId`/`image` contract on the +> **user-app** side. `user-app` consumes it; `user-app-v2` does **not** (gap V2-03). +> Closing that gap in v2 is additive and does not touch this admin code. + +--- + +## C. User apps — new designs and new features · **[brief]** + +Both `apps/user-app` and `apps/user-app-v2` keep their own visual design. The +following were identified in Phase 0 as design/feature additions with no +`henza_pwa` equivalent. + +### C.1 Shared by both apps + +| Item | Location | +|---|---| +| `/notifications` route + screen (self-documented "no legacy equivalent") | `views/user/NotificationsView.vue` | +| `ui/PageBar.vue` replacing the legacy `AppHeader` | `components/ui/` | +| Redesigned CSS bottom nav (5 tabs; legacy was a 4-tab GSAP notch) | `layout/` | +| `GallerySlider.vue` rewrite + teleported full-screen image preview | `components/product/` | +| Variant chips with per-variant images, stock badges, «نمایش همه» | `components/product/SingleProduct.vue` | +| The whole stock / pre-order rule (`qty`, `isPreOrder`, `capQty`, `maxQty`) | `stores/cartStore.ts`, `composables/useProduct.ts` — **v1 only; porting to v2 is gap MON-01** | +| vant pull-to-refresh on 5 screens | various views | +| IntersectionObserver incremental rendering (20-item window) | list views | +| Ledger-based «اسناد» screen | `views/user/TransactionsView.vue` | +| Order-detail drawer + 4-step tracker + `useOrderItemProduct` join | `views/user/OrdersView.vue` | +| 9-chip `OrderStatus` enum | `types/` | +| Dotlottie update dialog | app shell | +| New API contracts: `GET /home/slider`, `GoldPriceQuote`, `productCode`, `collections[]`, `gold-bank:*` session keys | `api/client.ts` | + +### C.2 `user-app` (design v1) only + +Home family: `components/home/RateStrip.vue`, `ShowcaseSlider.vue`, +`ShowcaseCard.vue`, `CategoryRail.vue`, `SectionHead.vue`, `AccountStrip.vue`; +admin banner via `GET /home/slider` + `AdminSlide`; the «برگزیده منتزر» and +«تازه‌ها» home sections; trust-badge rows. + +### C.3 `user-app-v2` (design v2) only + +`components/home/HeroCarousel.vue`, `ProductCarousel.vue`, `CategoryGrid.vue`, +`ProductPanel.vue`; the IRANSans FaNum font family (12 files, +`src/assets/styles/fonts/iransans/`); redesigned HomeView structure; redesigned +DashboardView (hero card + 4 settings rows, replacing card + tile grid); the +staggered first-paint reveal system (module-scope `hasRevealed`) in +HomeView/CartView/DashboardView; the `compact` RateStrip pill variant; +SingleProduct's scroll-driven solid header + category deep-link pill; the +pill-shaped SearchInput. + +> **Important boundary.** C.2 and C.3 are *design* differences and stay separate. +> The five items in `ISSUES.md` §V2-01..05 are *logic* differences and are **not** +> protected — `CLAUDE.md` §2 requires logic to be identical across both apps. + +--- + +## D. Backend — stricter auth than the reference · **[?] needs your ruling** + +**Not yet protected — Q1 in the gate.** + +The reference leaves every admin route unauthenticated (`henza_server/routes/admin.php:12` +and `:272` — the `auth:api` group wrapper is commented out at both ends, verified). +`apps/api` instead applies a global `JwtAuthGuard` + `PermissionsGuard` with +144 server-enforced rids, and only 15 routes opt out via `@Public()`. + +Strict parity would mean **removing** authentication from ~215 admin endpoints. +`CLAUDE.md` §3 already carves out exactly this case ("if matching the reference +would cause … a clear security hole in money flows → STOP and ask"). + +**Proposed:** protect the target's stricter auth as an accepted deviation; log the +reference behaviour as `SEC-01` and the residual `@Public()` routes as `SEC-02`. + +--- + +## E. Backend — Decimal money discipline · **[found] proposed** + +The reference uses PHP floats throughout. `apps/api` has **zero `Float` columns** +in `prisma/schema.prisma`; ledger and order math run on `Prisma.Decimal`. + +Reverting to floats for "parity" would introduce rounding error into money and +gram values. **Proposed:** protect the Decimal discipline. The one leak +(`GRAMS_PER_MESGHAL` as a JS float, `ledger.service.ts:189`) is logged as `MON-03`, +to be fixed in Phase 3 — not now. + +Note this does **not** exempt the target from matching the reference's *arithmetic*, +including the `floor(price/1000)*1000` rounding in `ProductVariant.php:183-190`. +Same result, better representation. + +--- + +## F. Prior deliberate non-ports · **[?] needs your ruling (Q4)** + +Recorded in `docs/CHANGELOG-PORT-HISTORY.md` as intentionally **not** ported: + +1. Unauthenticated file upload (`file/create` had no middleware in the reference) +2. SMS sent inside `toggleActive` +3. Six destructive data behaviours — deleting a confirmed document, editing a + confirmed amount, deleting a category that has dependents, creating a category + cycle, orphaning the audit trail — each replaced with a Persian 400 message +4. The reference's avatar-load crash (`ReagentCustomer.vue:280` dereferenced + `data.image.id` unguarded) +5. The branch-mode horizontal layout (`HorizontalNavMenu`, 4 files never ported; + `mainLayoutType` narrowed to `'vertical'`) + +Items 1–4 are security/data-loss carve-outs consistent with `CLAUDE.md` §3 and are +**proposed for protection**. **Item 5 is different** — it is a missing feature, not +a safety carve-out, and Phase 1 should treat it as a real gap unless you say +otherwise. + +--- + +## Summary — what needs your decision + +| Section | Item | Proposed | +|---|---|---| +| A | Admin Dashboard | protect (per brief) | +| B | Product Variants image picker | protect (per brief) | +| C | User-app designs + new features | protect (per brief), **excluding** the 5 v1↔v2 logic gaps | +| D | Stricter admin auth in `apps/api` | protect — **confirm (Q1)** | +| E | Decimal money types | protect — **confirm** | +| F.1–4 | Prior security / data-loss non-ports | protect — **confirm (Q4)** | +| F.5 | Branch horizontal layout | **treat as a real gap** — confirm | diff --git a/docs/parity/_inv-ref-admin.md b/docs/parity/_inv-ref-admin.md new file mode 100644 index 0000000..8b5a795 --- /dev/null +++ b/docs/parity/_inv-ref-admin.md @@ -0,0 +1,1705 @@ +# Inventory — henza_admin (reference admin) + +> Read-only, evidence-based inventory. All `path:line` are relative to `henza_admin/`. +> Source read at: repo root `E:\gold bank\henza_admin`. + +--- + +## 1. Stack & Versions + +All from `package.json`. + +| Concern | Package | Version | Line | +|---|---|---|---| +| Framework | `vue` | `^2.6.10` | package.json:47 | +| Router | `vue-router` | `^3.0.6` | package.json:65 | +| Store | `vuex` | `^3.1.1` | package.json:77 | +| UI lib (primary, tables/forms/modals) | `ant-design-vue` | `^1.6.0` | package.json:8 | +| UI lib (secondary, layout/theme/notify) | `vuesax` | `3.11.13` | package.json:76 | +| HTTP client | `axios` | `^0.19.0` | package.json:11 | +| Date (Jalali) | `moment-jalaali` | `^0.9.2` | package.json:29 | +| Date picker (Jalali) | `vue-persian-datetime-picker` | `^2.4.1` | package.json:56 | +| Charts | `apexcharts` `^3.36.0` + `vue-apexcharts` `^1.6.2` | | package.json:7, 48 | +| Charts (alt) | `chart.js` `^2.8.0` + `vue-chartjs` `^3.4.2` | | package.json:12, 51 | +| Charts (alt) | `echarts` `^4.2.1` + `vue-echarts` `^4.0.3` | | package.json:15, 54 | +| Grid (unused legacy) | `ag-grid-community` / `ag-grid-vue` | `^28.1.1` | package.json:4-5 | +| Validation (legacy) | `vee-validate` | `^2.2.8` | package.json:46 — registered main.js:84-85; used only in `views/auth/Login.vue`. Business forms use ant-design `v-decorator` rules. | +| ACL | `vue-acl` | `4.0.7` | package.json:49 | +| Excel export | `xlsx` `^0.15.0`, `file-saver` `2.0.1` | | package.json:78, 16 | +| i18n | `vue-i18n` | `^8.11.2` | package.json:58 | +| CSS | `tailwindcss` `^1.0.1` + `postcss-rtl` `1.5.0` + `sass` | | package.json:84, 32 | +| Firebase (legacy, auth store only) | `firebase` | `^9.6.7` | package.json:17 | +| Node | `node:^14--npm:6` | | package.json:3 | +| App version | `5.0.1` | | package.json:3 | + +**Build modes** — `vue.config.js:3-4`: `publicPath` = `/branch/` when `NODE_ENV=='branch'`, else `admin/`; `outputDir` = `branchDist/export` vs `dist/export`. LESS `modifyVars`: `primary-color:#1c2d56`, `secondary-color:#836447`, `third-color:#f5cb55`, `border-radius-base:6px` (vue.config.js:15-20). + +--- + +## 2. HTTP Layer + +### 2.1 `src/axios.js` (primary instance — used everywhere) + +| Aspect | Value | Evidence | +|---|---|---| +| Base URL | `https://henzagold.com/oapi/admin/` (hard-coded, exported as `baseURL`) | src/axios.js:5 | +| Token storage key | `branch_token` if `isBranch()` else `token` | src/axios.js:6 | +| Default headers | `Content-Type: application/json`, `Accept: application/json` | src/axios.js:7-11 | +| Initial auth header | `Authorization: Bearer ` set at module load if token exists | src/axios.js:13-17 | +| Request interceptor | Overwrites `config.headers["Authorization"] = "Bearer " + localStorage.getItem(tokenName)` on **every** request | src/axios.js:24-32 | +| Request error handler | `Promise.reject(error)` — **not returned** (bug: interceptor returns `undefined`) | src/axios.js:29-31 | +| Response interceptor | **None on this instance** | src/axios.js (absent) | +| Exports | `default ax`, named `baseURL`, `token` | src/axios.js:5, 33-34 | +| Registered as | `Vue.prototype.$http = axios` | src/main.js:19-20 | + +### 2.2 `src/http/*` (Vuexy template leftovers — DEAD CODE) + +| File | Content | Wired in? | +|---|---|---| +| `src/http/axios/index.js:1-3` | re-exports `src/axios.js` | yes (by jwt module) | +| `src/http/requests/index.js:1` | `import './auth'` | **No** — `import './http/requests'` is commented out at src/main.js:22-23 | +| `src/http/requests/auth/index.js:1` | `import './jwt/index.js'` | dead | +| `src/http/requests/auth/jwt/index.js` | see below | dead | + +`src/http/requests/auth/jwt/index.js` — **never invoked** (`init()` has no caller): +- Response interceptor on 401 (src/http/requests/auth/jwt/index.js:26-43): single-flight refresh via `isAlreadyFetchingAccessToken` flag + `subscribers` queue (lines 5-14); dispatches `auth/fetchAccessToken`; retries original request with new bearer. +- `login(email,pwd)` → `POST /api/auth/login` (line 48) +- `registerUser(name,email,pwd)` → `POST /api/auth/register` (line 54) +- `refreshToken()` → `POST /api/auth/refresh-token` with `{accessToken: localStorage.accessToKen}` (line 61 — note typo `accessToKen`) +- `store/auth/moduleAuthActions.js:357-361` `fetchAccessToken` wraps `jwt.refreshToken()` — also dead (real auth module is `store/auth/moduleAuth.js`). + +**Net effect: there is NO refresh-token flow and NO response interceptor in the live app.** 401s bubble to each caller's `try/catch`. + +### 2.3 Error handling / toasts + +Two mechanisms coexist: + +| Mechanism | Definition | Usage | +|---|---|---| +| ant-design `this.$message.*` | global via `Vue.use(Antd)` (src/main.js:3,16) | e.g. `this.$message.info('ویرایش موفق آمیز')` / `this.$message.error('خطا در ویرایش')` — src/views/order/List.vue:262,265,273,276 | +| Vuesax notify wrapper plugin | `src/notify.js` (`Vue.use(notify)` at src/main.js:107-108) | 5 prototype methods below | + +`src/notify.js` methods: + +| Method | Title | Default text | Color | Line | +|---|---|---|---|---| +| `$ntSetError(msg)` | `خطا در ثبت اطلاعات` | `اطلاعات ثبت نگردید.` | `danger` | notify.js:4-10 | +| `$ntSet(msg)` | `اطلاعات ثبت شد.` | `اطلاعات با موفقیت به ثبت رسید` | `primary` | notify.js:12-18 | +| `$ntNoTitle(msg)` | — | `اطلاعات با موفقیت به ثبت رسید` | `primary` | notify.js:19-24 | +| `$ntRemove(msg)` | `اطلاعات با موفقیت حذف شد` | `''` | `warning` | notify.js:27-33 | +| `$ntGetError(msg)` | `خطا در ثبت اطلاعات` | `در اطلاعات دریافت شده خطایی رخ داده ` | `danger` | notify.js:35-41 | + +--- + +## 3. Router + +`src/router.js` builds the tree; route arrays live in `src/router/admin.js` (admin build) and `src/router/branch.js` (branch build). `src/router/{apps,category,customer,dashboard,full,manage,order,product,temp}.js` exist but are **not imported** by `src/router.js` (only `./router/admin` and `./router/branch` are — src/router.js:6,8) → dead. + +### 3.1 Router config + +| Item | Value | Line | +|---|---|---| +| mode | `history` | router.js:15 | +| base | `process.env.BASE_URL` | router.js:16 | +| scrollBehavior | always `{x:0,y:0}` | router.js:17-19 | +| Main layout | `layouts/main/Main.vue`, children = `branchRoute` or `adminRoute` per `isBranch()` | router.js:25-28 | +| Full-page layout | `layouts/full-page/FullPage.vue`, children = `fullPageBranchRoute`/`fullPageAdminRoute` + 4 error pages | router.js:32-138 | +| catch-all | `*` → redirect `/pages/error-404` | router.js:140-143 | +| afterEach | hides `#loading-bg` | router.js:147-153 | + +### 3.2 Guard (`router.beforeEach`) — src/router.js:184-199 + +1. Whitelist `noneAuthUrl = ['/auth/login','/auth/forgot-password','/auth/register','/reg']` → `next()` (router.js:156-161, 185-187). +2. Read token from `localStorage[isBranch()?'branch_token':'token']` (router.js:188). +3. If no token → `router.push('/auth/login')` (router.js:198). +4. If token and module-level `roles` is unset → `await getPermission()` (router.js:190-191). +5. `getPermission()` (router.js:163-182): if `store.state.auth.token && store.state.auth.user` and no cached roles → `GET users/show`, dispatch `users/getRoles(data.permission)`, delete `permission`+`ut` from payload, write `localStorage[token|branch_token] = data.api_token` and `localStorage[user|branch_user] = JSON.stringify(data)`. +6. If `to.meta.role` exists and `!roles.includes(to.meta.role)` → redirect to route name `page-not-authorized` (router.js:193-195). + +Note: `roles` is a **module-level** variable cached for the page lifetime (router.js:162). + +### 3.3 Admin routes — `src/router/admin.js` (`adminRoute`, main layout) + +`meta.rule` is the vue-acl rule; `meta.role` is the real permission gate; `meta.parent` / `meta.no_scroll` are layout hints; `meta.pageTitle` + `meta.breadcrumb` drive the header. + +| path | name | component file | meta.role | meta.rule | meta.pageTitle | other meta | line | +|---|---|---|---|---|---|---|---| +| `/` | `dashboard` | *(redirect → `/my-data`)* | — | — | — | — | admin.js:3-5 | +| `/dashboard/analytics` | — | `views/DashboardAnalytics.vue` | `dashboard.tak` | editor | — | — | admin.js:8-15 | +| `/my-data` | `my-data` | `views/user/myData.vue` | *(none)* | editor | — | — | admin.js:17-24 | +| `/dashboard/ecommerce` | — | `views/DashboardECommerce.vue` | `dashboard.omde` | admin | — | — | admin.js:26-33 | +| `/config/manage/const` | — | `views/manage/Const.vue` | `config.statics` | editor | مدیریت ثابت ها | parent `manage`, no_scroll | admin.js:34-54 | +| `/config/manage/wallet` | — | `views/manage/wallet.vue` | `config.wallet` | editor | مدیریت کیف پول | parent `manage`, no_scroll | admin.js:55-75 | +| `/config/manage/report` | — | `views/manage/report.vue` | `config.report` | editor | گزارش | parent `manage`, no_scroll | admin.js:76-96 | +| `/apps/product/` | `product` | *(redirect → `/apps/product/list`)* | — | — | — | — | admin.js:98-102 | +| `/product/change/percent` | — | `views/apps/product/ChangePercent.vue` | `product.percent` | editor | — | parent `product`, no_scroll | admin.js:103-112 | +| `/apps/product/add` | — | `views/apps/product/AddProduct.vue` | `product.create` | editor | — | parent `product`, no_scroll | admin.js:113-122 | +| `/apps/product/edit/:id/:variant_id?` | `product-edit` | `views/apps/product/EditProduct.vue` | `product.list.edit` | editor | — | parent `product`, no_scroll | admin.js:123-133 | +| `/apps/product/list` | `product-list` | `views/apps/product/ProductList.vue` | `product.list` | editor | — | parent `product`, no_scroll | admin.js:134-144 | +| `/category` | `category` | *(redirect → `/category/list`)* | — | — | — | — | admin.js:146-150 | +| `/category/add/:id?` | `category-add` | `views/apps/category/Add.vue` | `collection.create` | editor | — | parent `category`, no_scroll | admin.js:151-161 | +| `/category/edit/:id` | `category-edit` | `views/apps/category/Edit.vue` | `collection.list.edit` | editor | — | parent `category`, no_scroll | admin.js:162-172 | +| `/category/list` | `category-list` | `views/apps/category/List.vue` | `collection.list` | editor | — | parent `category`, no_scroll | admin.js:173-183 | +| `/add/findoc` | — | `views/finical/addFinDoc` | `document.create` | editor | — | — | admin.js:185-192 | +| `/edit/findoc/:id` | — | `views/finical/addFinDoc` | `document.list.edit` | editor | — | — | admin.js:194-201 | +| `/order` | `order` | *(redirect → `/order/list`)* | — | — | — | — | admin.js:204-208 | +| `/order/list` | — | `views/order/List.vue` | `order.list` | editor | — | parent `order`, no_scroll | admin.js:209-218 | +| `/order/omde-list` | — | `views/order/OmdeList.vue` | `order.omde` | editor | — | parent `order`, no_scroll | admin.js:219-228 | +| `/order/make` | — | `views/order/MakeOrder.vue` | `order.makeOrder` | editor | — | parent `order`, no_scroll | admin.js:229-238 | +| `/order/create` | — | `views/order/create.vue` | `order.create` | editor | — | parent `order`, no_scroll | admin.js:239-248 | +| `/order/tamin-list` | — | `views/order/TaminList.vue` | `order.tamin` | editor | — | parent `order`, no_scroll | admin.js:249-258 | +| `major/promotion/list` *(relative → `/major/promotion/list`)* | — | `views/promotion/promotion_list` | `offers.list` | editor | — | no_scroll | admin.js:259-268 | +| `major/promotion/add` | — | `views/promotion/promotion_add_edit` | `offers.create` | editor | — | no_scroll | admin.js:269-278 | +| `major/promotion/edit/:id` | — | `views/promotion/promotion_add_edit` | `offers.list.edit` | editor | — | no_scroll | admin.js:279-288 | +| `major/promotion/tak/add` | — | `views/promotion/promotion_tak_add_edit` | `offers.create.tak` | editor | — | no_scroll | admin.js:289-298 | +| `major/promotion/tak/edit/:id` | — | `views/promotion/promotion_tak_add_edit` | `offers.list.tak.edit` | editor | — | no_scroll | admin.js:299-308 | +| `major/promotion/autopromotion` | — | `views/promotion/autopromotion` | `offers.autopromotion` | editor | — | no_scroll | admin.js:309-317 | +| `major/offersIgnore` | — | `views/offersIgnore/offersIgnore-add-edit.vue` | `offersIgnore` | editor | — | — | admin.js:319-326 | +| `/comments` | `comments` | `views/customer/Comments/Comments.vue` | `comment` | editor | دیدگاه ها | breadcrumb | admin.js:328-348 | +| `/sms/create` | `create_sms` | `views/sms/create.vue` | `sms.create` | editor | ارسال پیامک | breadcrumb | admin.js:350-370 | +| `/notify` | `notify` | `views/notify/user.vue` | `notify.me` | editor | لیست نوتیفیکیشن های ذریافتی | breadcrumb | admin.js:371-391 | +| `/notify/create` | `create_notify` | `views/notify/create.vue` | `notify.create` | editor | ارسال نوتیفیکیشن | breadcrumb | admin.js:392-412 | +| `/wallet` | `wallet` | `views/wallet/wallet.vue` | `wallet` | editor | لیست کیف پول های شارژ شده | breadcrumb | admin.js:413-433 | +| `/reagent/edit/:id` | `reagent-edit` | `views/apps/customer/EditReagent` | `persons.list.sellers.edit` | editor | — | parent `product`, no_scroll | admin.js:434-444 | +| `/bulkCustomer/edit/:id` | `bulkCustomer-edit` | `views/apps/customer/EditBulkCustomer` | `persons.list.client.omde.edit` | editor | — | parent `product`, no_scroll | admin.js:445-455 | +| `/bulkCustomer/view/:id` | `bulkCustomer-view` | `views/customer/omde/ViewOmdeCustomer` | `persons.list.client.omde.view` | editor | — | parent `product`, no_scroll | admin.js:456-466 | +| `/singleCustomer/edit/:id` | `singleCustomer-edit` | `views/apps/customer/EditSingleCustomer` | `persons.list.client.single.edit` | editor | — | parent `product`, no_scroll | admin.js:467-477 | +| `/singleCustomer/view/:id` | `singleCustomer-view` | `views/customer/single-customer/ViewSingleCustomer` | `persons.list.client.single.view` | editor | — | parent `product`, no_scroll | admin.js:478-488 | +| `/customer/omde-list` | `omde-list-customer` | `views/customer/omde/OmdeList.vue` | `persons.list.client.omde` | editor | مشتریان عمده | breadcrumb | admin.js:489-507 | +| `/customer/omde-non-verified-list` | `omde-non-verified-list` | `views/customer/omde/OmdeNonVerifiedList.vue` | `persons.list.client.omde` | editor | مشتریان در انتظار | breadcrumb | admin.js:509-527 | +| `/customer/banker-list` | `banker-list` | `views/customer/omde/BankerList.vue` | `persons.list.client.omde` | editor | لیست بنکداران | breadcrumb | admin.js:529-547 | +| `/customer/shopkeeper-list` | `shopkeeper-list` | `views/customer/omde/ShopkeeperList.vue` | `persons.list.client.omde` | editor | لیست مغازه داران حضوری | breadcrumb | admin.js:549-567 | +| `/customer/omde-archived-list` | `omde-archived-list` | `views/customer/omde/OmdeArchivedList.vue` | `persons.list.client.omde` | editor | مشتریان بایگانی | breadcrumb | admin.js:569-587 | +| `/customer/reagent-list` | `raegentList` | `views/customer/reagentList/ReagentList.vue` | `persons.list.sellers` | editor | کارشناسان فروش | breadcrumb | admin.js:589-607 | +| `/customer/add` | `add-customer` | `views/apps/customer/AddCustomer.vue` | `persons.create` | editor | افزودن مشتری | breadcrumb | admin.js:609-628 | +| `/customer/list` | `list-customer` | `views/customer/single-customer/SingleCustomer` | `persons.list.client.single` | editor | لیست مشتریان | breadcrumb | admin.js:630-645 | +| `/report/order_item` | `report_order_item` | `views/report/order_item` | `report.order.item` | editor | — | — | admin.js:647-659 | +| `/report/product` | `report_product` | `views/report/product` | `report.product` | editor | — | — | admin.js:661-669 | +| `/finical/list` | `finical_list` | `views/finical/list` | `document.list` | editor | — | — | admin.js:670-683 | +| `/user/list` | — | `views/user/ListUser.vue` | `user.list` | editor | لیست مدیران | breadcrumb, no_scroll | admin.js:685-704 | +| `/user/create` | — | `views/user/UserCreate.vue` | `user.create` | editor | افزودن کاربر | breadcrumb, no_scroll | admin.js:706-725 | +| `/user/edit/:id` | — | `views/user/UserEdit.vue` | `user.list.edit` | editor | ویرایش نوع کاربر | breadcrumb, no_scroll | admin.js:727-747 | +| `/permission` | — | `views/user/Permission.vue` | `user.permission` | editor | دسترسی ها | breadcrumb, no_scroll | admin.js:749-768 | +| `/city/create` | — | `views/city/createCity.vue` | `cities.create` | editor | افزودن شهر | breadcrumb, no_scroll | admin.js:770-789 | +| `/city/list` | — | `views/city/cityList.vue` | `cities.list` | editor | لیست شهر ها | breadcrumb, no_scroll | admin.js:791-811 | +| `/statics` | `statics` | `views/statics/index.vue` | `statics` | editor | آمار | breadcrumb | admin.js:813-832 | +| `/newsletters` | `newsletters` | `views/newsletters/newsletters.vue` | *(commented out `newsletters`)* | editor | خبرنامه | breadcrumb | admin.js:834-853 | +| `/branch/add` | `add-branch` | `views/branch/AddBranches.vue` | `branch.create` | editor | افزودن شعب | breadcrumb | admin.js:855-874 | +| `/branch/edit/:id` | `edit-branch` | `views/branch/EditBranches` | `branch.create` | editor | ویرایش شعب | — | admin.js:876-885 | +| `/branch/list` | `list-branch` | `views/branch/ListBranches.vue` | `branch.list` | editor | لیست شعب | breadcrumb | admin.js:886-906 | +| `/workshop/add` | `add-workshop` | `views/workshop/AddWorkshop.vue` | `workshop.create` | editor | افزودن کارگاه | breadcrumb | admin.js:908-927 | +| `/workshop/edit/:id` | `edit-workshop` | `views/workshop/EditWorkshop` | `workshop.create` | editor | ویرایش کارگاه | — | admin.js:929-938 | +| `/workshop/list` | `list-workshop` | `views/workshop/ListWorkshop.vue` | `workshop.list` | editor | لیست کارگاه ها | breadcrumb | admin.js:939-959 | +| `/invoice/add` | `add-invoice` | `views/invoice/AddInvoice.vue` | `invoice.create` | editor | افزودن فاکتور | breadcrumb | admin.js:961-980 | +| `/invoice/edit/:id` | `edit-invoice` | `views/invoice/EditInvoice.vue` | `invoice.create` | editor | ویرایش فاکتورها | — | admin.js:982-991 | +| `/invoice/list` | `list-invoice` | `views/invoice/ListInvoice.vue` | `invoice.list` | editor | لیست فاکتورها | breadcrumb | admin.js:992-1012 | + +**73 routes** in `adminRoute` (incl. 4 redirect-only entries). + +### 3.4 Admin full-page routes — `fullPageAdminRoute` (src/router/admin.js:1016-1033) + +| path | name | component | meta | line | +|---|---|---|---|---| +| `/auth/login` | `page-login` | `views/auth/Login.vue` | rule `editor` | admin.js:1017-1024 | +| `/reg` | `reg` | `views/reg.vue` | rule `editor` | admin.js:1025-1032 | + +### 3.5 Shared full-page error routes — `src/router.js:104-135` + +| path | name | component | meta.rule | +|---|---|---|---| +| `/pages/error-404` | `page-error-404` | `views/pages/Error404.vue` | editor (router.js:104-111) | +| `/pages/error-500` | `page-error-500` | `views/pages/Error500.vue` | editor (router.js:112-119) | +| `/pages/not-authorized` | `page-not-authorized` | `views/pages/NotAuthorized.vue` | editor (router.js:120-127) | +| `/pages/maintenance` | `page-maintenance` | `views/pages/Maintenance.vue` | editor (router.js:128-135) | +| `*` | — | redirect `/pages/error-404` | router.js:140-143 | + +### 3.6 Branch build routes — `src/router/branch.js` + +| path | name | component | meta.role | meta.rule | line | +|---|---|---|---|---|---| +| `/` | `main` | redirect → `/dashboard` | — | — | branch.js:2-6 | +| `/dashboard` | `dashboard` | `views/branch/dashboard.vue` | `dashboard.omde` | admin | branch.js:7-15 | +| `/products` | `product-list` | `views/apps/product/ProductList.vue` | `product.list` | editor | branch.js:16-26 | +| `/order/list` | — | `views/order/List.vue` | `order.list` | editor | branch.js:32-41 | +| `/buy/order/list` | — | `views/branch/buyList.vue` | `order.list` | editor | branch.js:42-51 | +| `/order/create` | — | `views/branch/order/create.vue` | `order.create` | editor | branch.js:61-70 | +| `/singleCustomer/view/:id` | `singleCustomer-view` | `views/customer/single-customer/ViewSingleCustomer` | `persons.list.client.single.view` | editor | branch.js:82-92 | +| `/singleCustomer/edit/:id` | `singleCustomer-edit` | `views/apps/customer/EditSingleCustomer` | `persons.list.client.single.edit` | editor | branch.js:93-103 | +| `/customer/add` | `add-customer` | `views/branch/addCustomer.vue` | `persons.create` | editor | branch.js:104-112 | +| `/customer/list` | `list-customer` | `views/customer/single-customer/SingleCustomer` | `persons.list.client.single` | editor | branch.js:113-121 | +| `/auth/login` (full-page) | `page-login` | `views/branch/login.vue` | — | editor | branch.js:161-169 | + +--- + +## 4. Menu / Navigation + +Defined in **`src/layouts/components/vertical-nav-menu/navMenuItems.js`**; exports `isBranch() ? branchMenu : adminMenu` (navMenuItems.js:609-611). Consumed by `VerticalNavMenu.vue` (vertical layout) and `HorizontalNavMenu.vue` (branch/horizontal layout — `themeConfig.js:23` sets `mainLayoutType` to `horizontal` for branch). + +**Gating**: `VerticalNavMenu.vue:322` — `if ((item.permissionId && this.$hasPermission(item.permissionId)) || !item.permissionId)`. Additional server-driven removal at `VerticalNavMenu.vue:187-220` (`GET /roles/{ut}` → `removeItem` list filters by `permissionId`). + +### 4.1 Admin menu + +| Group (header / i18n) | Item i18n | url | slug | icon | permissionId | line | +|---|---|---|---|---|---|---| +| — | **پیشخوان** | null | — | `HomeIcon` | `dashboard` | 3-10 | +| ↳ | آمار عمده فروشی | `/dashboard/ecommerce` | dashboard-ecommerce | — | `dashboard.omde` | 12-18 | +| ↳ | آمار تک فروشی | `/dashboard/analytics` | dashboard-ecommerce | — | `dashboard.tak` | 19-25 | +| `Apps` / کاربری (`PackageIcon`) | **اشخاص** | null | — | `UserIcon` | `persons` | 33-38 | +| ↳ | اضافه کردن شخص | `/customer/add` | add-customer | — | `persons.create` | 40-46 | +| ↳ | لیست کارشناسان فروش | `/customer/reagent-list` | raegentList | — | `persons.list.sellers` | 47-53 | +| ↳ | لیست مشتریان عمده | `/customer/omde-list` | omde-customer-list | — | `persons.list.client.omde` | 54-60 | +| ↳ | لیست مشتریان در انتظار | `/customer/omde-non-verified-list` | omde-non-verified-list | — | `persons.list.client.omde` | 61-67 | +| ↳ | لیست مشتریان بایگانی | `/customer/omde-archived-list` | omde-archived-list | — | `persons.list.client.omde` | 68-74 | +| ↳ | لیست مشتریان تک | `/customer/list` | app-user-list | — | `persons.list.client.single` | 75-81 | +| ↳ | لیست بنکداران | `/customer/banker-list` | banker-list | — | **none** (commented) | 82-88 | +| ↳ | لیست مغازه داران حضوری | `/customer/Shopkeeper-list` *(capital S — route is `/customer/shopkeeper-list`)* | shopkeeper-list | — | **none** (commented) | 89-95 | +| | **شعب** | null | — | `GridIcon` | `branch` | 98-103 | +| ↳ | افزودن شعب | `/branch/add` | add-branch | — | `branch.create` | 105-111 | +| ↳ | لیست شعب | `/branch/list` | branch-list | — | `branch.list` | 112-118 | +| | **کارگاه** | null | — | `GridIcon` | `workshop` | 122-127 | +| ↳ | افزودن کارگاه | `/workshop/add` | add-workshop | — | `workshop.create` | 129-135 | +| ↳ | لیست کارگاه ها | `/workshop/list` | workshop-list | — | `workshop.list` | 136-142 | +| | **محصولات** | null | — | `ShoppingBagIcon` | `product` | 146-151 | +| ↳ | افزودن محصول | `/apps/product/add` | add-product | — | `product.create` | 153-159 | +| ↳ | محصولات | `/apps/product/list` | product-list | — | `product.list` | 160-166 | +| ↳ | تغییر درصد محصولات | `/product/change/percent` | change-Percent | — | `product.percent` | 167-173 | +| | **سفارش ها** | null | — | `BoxIcon` | `order` | 176-181 | +| ↳ | ثبت سفارش تک | `/order/create` | order-omde-list | — | `order.create` | 183-189 | +| ↳ | لیست سفارش های تک | `/order/list` | order-list | — | `order.list` | 190-196 | +| ↳ | سفارش های ساختنی | `/order/make` | order-makeOrder | — | `order.makeOrder` | 197-203 | +| ↳ | لیست سفارش های عمده | `/order/omde-list` | order-omde-list | — | `order.omde` | 204-210 | +| ↳ | لیست تامین | `/order/tamin-list` | order-tamin-list | — | `order.tamin` | 211-217 | +| | **مدیران** | null | — | `UserIcon` | `user` | 220-225 | +| ↳ | دسترسی ها | `/permission` | user-permission | — | `user.permission` | 227-233 | +| ↳ | افزودن مدیر | `/user/create` | add-user | — | `user.create` | 234-240 | +| ↳ | لیست مدیران | `/user/list` | user-list | — | `user.list` | 241-247 | +| | **کیف پول** | null | — | `CreditCardIcon` | `wallet` | 250-255 | +| ↳ | لیست کیف پول های شارژ شده | `/wallet` | wallet | — | `wallet.list` | 257-263 | +| | **دسته بندی ها** | null | — | `ListIcon` | `collection` | 266-271 | +| ↳ | افزودن دسته بندی | `/category/add` | add-category | — | `collection.create` | 273-279 | +| ↳ | لیست دسته بندی ها | `/category/list` | list-category | — | `collection.list` | 280-286 | +| | **اسناد مالی** | null | — | `DollarSignIcon` | `document` | 289-294 | +| ↳ | لیست اسناد | `/finical/list` | finical_list | — | `document.list` | 296-302 | +| ↳ | وارد کردن سند مالی مشتری | `/add/findoc` | findoc | — | `document.create` | 303-309 | +| | **تخفیفات** | null | — | `GiftIcon` | `offers` | 312-317 | +| ↳ | تعریف تخفیف تک | `/major/promotion/tak/add` | major_promotion_add | — | `offers.create.tak` | 319-325 | +| ↳ | تخفیف اتوماتیک | `/major/promotion/autopromotion` | major_promotion_list | — | `offers.autopromotion` | 326-332 | +| ↳ | تعریف تخفیف عمده | `/major/promotion/add` | major_promotion_add | — | `offers.create` | 333-339 | +| ↳ | لیست تخفیفات | `/major/promotion/list` | major_promotion_list | — | `offers.list` | 340-346 | +| | **استثناعات تخفیف** | `/major/offersIgnore` | offersIgnore | `SlashIcon` | `offersIgnore` | 349-356 | +| | **فاکتور انتقال** | null | — | `BookIcon` | `invoice` | 358-363 | +| ↳ | افزودن فاکتور | `/invoice/add` | add-invoice | — | `invoice.create` | 365-371 | +| ↳ | لیست فاکتورها | `/invoice/list` | invoice-list | — | `invoice.list` | 372-378 | +| | **گزارشات** | null | — | `FileTextIcon` | `report` | 382-387 | +| ↳ | گزارش تجمیعی سفارش و اسناد | `/report/order_item` | report_ordedr_item | — | `report.order.item` | 389-395 | +| ↳ | گزارش محصولات | `/report/product` | report_product | — | `report.product` | 396-402 | +| | **شهر ها** | null | — | `PercentIcon` | `cities` | 405-411 | +| ↳ | افزودن شهر | `/city/create` | create_city | — | `cities.create` | 413-419 | +| ↳ | لیست شهر ها | `/city/list` | city_list | — | `cities.list` | 420-426 | +| | **دیدگاه ها** | `/comments` | — | `MessageSquareIcon` | `comment` | 429-436 | +| | **پیامک** | null | — | `MailIcon` | `sms` | 437-442 | +| ↳ | ارسال پیامک | `/sms/create` | create_sms | — | `sms.create` | 444-450 | +| | **نوتیفیکیشن** | null | — | `BellIcon` | `notify` | 453-458 | +| ↳ | ارسال نوتیفیکیشن | `/notify/create` | create_notify | — | `notify.create` | 460-466 | +| | **آمار** | `/statics` | — | `BarChartIcon` | `statics` | 469-475 | +| | **خبرنامه** | `/newsletters` | — | `FileTextIcon` | `newsletters.newsletters` | 476-482 | +| `UI` / مدیریت (`LayersIcon`) | **پیکر بندی** | null | — | `SettingsIcon` | `config` | 490-495 | +| ↳ | مدیریت ثابت ها | `/config/manage/const` | manage-const | — | `config.statics` | 497-503 | +| ↳ | گزارش | `/config/manage/report` | manage-report | — | `config.report` | 504-510 | +| ↳ | مدیریت کیف پول | `/config/manage/wallet` | manage-const | — | `config.wallet` | 511-517 | + +### 4.2 Branch menu — `navMenuItems.js:524-607` + +| i18n | url | icon | permissionId | line | +|---|---|---|---|---| +| پیشخوان | `/dashboard` | `HomeIcon` | `dashboard` | 525-532 | +| اشخاص (group) | null | `UserIcon` | `persons` | 533-538 | +| ↳ اضافه کردن مشتری | `/customer/add` | `UserPlusIcon` | `persons.create` | 540-547 | +| ↳ لیست مشتریان | `/customer/list` | `UsersIcon` | `persons.list.client.single` | 548-555 | +| محصولات | `/products` | `ShoppingBagIcon` | `product.list` | 558-565 | +| فاکتور خرید | `/buy/order/list` | `ShoppingBagIcon` | `product.list` | 566-573 | +| سفارش ها (group) | null | `BoxIcon` | `order` | 582-587 | +| ↳ ثبت سفارش | `/order/create` | `InboxIcon` | `order.create` | 589-596 | +| ↳ لیست سفارش | `/order/list` | `ListIcon` | `order.list` | 597-604 | + +--- + +## 5. ACL / Permissions + +### 5.1 Two independent systems + +**A. `vue-acl` (`src/acl/acl.js`)** — mostly vestigial. + +| Item | Value | Line | +|---|---|---| +| Install | `Vue.use(AclInstaller)` | acl.js:5 | +| `initialRole` | `'admin'`, overridden by `JSON.parse(localStorage.userInfo).userRole` | acl.js:7-10 | +| `notfound` | `/pages/not-authorized` | acl.js:14 | +| `acceptLocalRules` | `true` | acl.js:16 | +| globalRules | `admin: AclRule('admin')`; `editor: AclRule('editor').or('admin')` | acl.js:17-20 | + +Every route carries `meta.rule` of `'editor'` (or `'admin'` on the two dashboards) — see §3.3. Since the seeded role is `admin` and `editor` ORs `admin`, this never blocks anything in practice. + +**B. Real permission system (`$hasPermission` + `meta.role`)** — the operative one. + +| Piece | Definition | +|---|---| +| Check fn | `Vue.prototype.$hasPermission = (permission_id) => { const roles = store.state.users.roles; if(roles){ return roles.includes(permission_id) } }` — src/main.js:142-148 | +| Store of roles | `store.state.users.roles` (`undefined` until loaded) — src/store/users/index.js:26 | +| Loaded on login | `store/auth/moduleAuth.js:17` → `dispatch('users/getRoles', data.roles.map(x=>x.rid))` from `POST admin-login` | +| Loaded on refresh | `src/router.js:170-172` → `GET users/show` → `dispatch('users/getRoles', data.permission)` | +| Parent expansion | `users/getRoles` runs `checkParent(roles)` which, for each dotted key, adds every ancestor prefix — e.g. `order.list.sts.remove` implies `order.list.sts`, `order.list`, `order` — src/store/users/index.js:3-21, 64-66 | +| Route gate | `to.meta.role` checked in `beforeEach` — src/router.js:193-195 | +| Menu gate | `item.permissionId` — VerticalNavMenu.vue:322 | +| Button gate | `v-if="$hasPermission('…')"` in templates | +| Clear | `deleteRoles` mutation — src/store/users/index.js:44-46, 67-69 | + +### 5.2 Complete permission catalog — `src/views/user/permissionConst.js` + +Tree used by the permission editor (`views/user/Permission.vue`) as `a-tree` data. **172 declared keys** (a few key strings repeat with different titles, noted below). + +| key | title (fa) | line | +|---|---|---| +| `dashboard` | پیشخوان | 4 | +| `dashboard.omde` | آمار عمده فروشی | 8 | +| `dashboard.tak` | آمار تک فروشی | 12 | +| `persons` | اشخاص | 18 | +| `persons.create` | اضافه کردن شخص | 22 | +| `persons.create.seller` | افزودن کارشناس فروش | 26 | +| `persons.create.client` | افزودن مشتری عمده | 30 | +| `persons.create.single` | افزودن مشتری تک | 34 | +| `persons.list.sellers` | لیست کارشناسان فروش | 40 | +| `persons.list.sellers.edit` | ویرایش اطلاعات کارشناس فروش | 44 | +| `persons.list.sellers.view.orders` | مشاهده سفارشات کارشناس فروش | 48 | +| `persons.list.sellers.view.documents` | مشاهده اسناد مالی کارشناس فروش | 52 | +| `persons.list.sellers.unable.order` | توقف سفارش کارشناس فروش | 56 | +| `persons.list.sellers.able.order` | فعال سازی سفارش کارشناس فروش | 60 | +| `persons.list.sellers.able` | فعال سازی حساب کاربری کارشناس فروش | 64 | +| `persons.list.sellers.unable` | غیر فعال سازی حساب کاربری کارشناس فروش | 68 | +| `persons.list.client.omde` | لیست مشتریان عمده | 74 | +| `persons.list.client.omde.view` | مشاهده اطلاعات مشتریان عمده | 78 | +| `persons.list.client.omde.edit` | ویرایش اطلاعات مشتری عمده | 82 | +| `persons.list.client.omde.view.orders` | مشاهده سفارشات مشتری عمده | 86 | +| `persons.list.client.omde.view.documents` | مشاهده اسناد مالی مشتری عمده | 90 | +| `persons.list.client.omde.unable.order` | توقف سفارش مشتری عمده | 94 | +| `persons.list.client.omde.able.order` | فعال سازی سفارش مشتری عمده | 98 | +| `persons.list.client.omde.able` | فعال سازی مشتری عمده | 102 | +| `persons.list.client.omde.unable` | غیر فعال سازی مشتری عمده | 106 | +| `persons.list.client.single` | لیست مشتریان تک | 112 | +| `persons.list.client.single.view` | مشاهده اطلاعات مشتریان تک *(duplicate key at 124: «مشاهده اطلاعات مشتری تک»)* | 116, 124 | +| `persons.list.client.single.edit` | ویرایش اطلاعات مشتری تک | 120 | +| `branch` | شعب | 132 | +| `branch.create` | افزودن شعب | 137 | +| `branch.list` | لیست شعب | 141 | +| `workshop` | کارگاه | 147 | +| `workshop.create` | افزودن کارگاه | 152 | +| `workshop.list` | لیست کارگاه ها | 156 | +| `product` | محصولات | 162 | +| `product.create` | افزودن محصول | 167 | +| `product.list` | لیست محصولات | 171 | +| `product.list.view` | مشاهده لیست محصولات *(duplicate at 184: «مشاهده اطلاعات جامع محصول»)* | 176, 184 | +| `product.list.edit` | ویرایش اطلاعات محصول | 180 | +| `product.list.variants` | تنوع های محصول | 188 | +| `product.list.variants.view` | مشاهده تنوع های محصول | 193 | +| `product.list.variants.edit` | ویرایش تنوع محصول | 197 | +| `product.list.variants.remove` | حذف تنوع محصول | 201 | +| `product.list.variants.add` | افزودن تنوع محصول | 205 | +| `product.list.remove` | حذف محصول | 211 | +| `product.list.group.edit` | تغییر دسته ای | 215 | +| `product.percent` | تغییر درصد محصولات | 221 | +| `order` | سفارشات | 227 | +| `order.list` | لیست سفارشات | 232 | +| `order.list.view` | مشاهده لیست سفارشات *(duplicate at 260: «مشاهده سفارش»)* | 237, 260 | +| `order.list.sts` | وضعیت سفارش | 241 | +| `order.list.sts.view` | مشاهده وضعیت سفارش | 246 | +| `order.list.sts.remove` | حذف وضعیت | 250 | +| `order.list.sts.submit` | ثبت وضعیت | 254 | +| `order.list.view.edit` | ویرایش سفارش | 264 | +| `order.list.pdf` | دریافت pdf | 268 | +| `order.omde` | سفارشات عمده | 274 | +| `order.omde.state` | وضعیت سفارش | 279 | +| `order.omde.state.view` | مشاهده وضعیت سفارش | 284 | +| `order.omde.state.submit` | ثبت وضعیت سفارش | 288 | +| `order.omde.state.remove` | حذف وضعیت سفارش | 292 | +| `order.omde.view` | مشاهده سفارش | 298 | +| `order.omde.view.edit` | ویرایش سفارش | 302 | +| `order.omde.view.remove` | حذف آیتم از سفارش | 306 | +| `order.omde.remove` | حذف سفارش | 310 | +| `order.omde.get` | دریافت pdf سفارش | 314 | +| `order.makeOrder` | سفارشات ساختنی | 320 | +| `order.makeOrder.state` | وضعیت سفارش | 325 | +| `order.makeOrder.state.view` | مشاهده وضعیت سفارش | 330 | +| `order.makeOrder.state.submit` | ثبت وضعیت سفارش | 334 | +| `order.makeOrder.state.remove` | حذف وضعیت سفارش | 338 | +| `order.makeOrder.view` | مشاهده سفارش | 344 | +| `order.makeOrder.view.edit` | ویرایش سفارش | 348 | +| `order.makeOrder.view.remove` | حذف آیتم از سفارش | 352 | +| `order.makeOrder.remove` | حذف سفارش | 356 | +| `order.makeOrder.get` | دریافت pdf سفارش | 360 | +| `order.tamin` | لیست تامین | 366 | +| `order.tamin.view` | مشاهده لیست تامین *(duplicate at 394: «مشاهده سفارش»)* | 371, 394 | +| `order.tamin.state` | وضعیت سفارش | 375 | +| `order.tamin.state.view` | مشاهده وضعیت سفارش | 380 | +| `order.tamin.state.submit` | ثبت وضعیت سفارش | 384 | +| `order.tamin.state.remove` | حذف وضعیت سفارش | 388 | +| `order.tamin.view.edit` | ویرایش سفارش | 398 | +| `order.create` | ثبت سفارش تک | 404 | +| `user` | مدیران | 410 | +| `user.create` | افزودن مدیر | 415 | +| `user.list` | لیست مدیران | 419 | +| `user.list.view` | مشاهده لیست مدیران | 424 | +| `user.list.edit` | ویرایش مدیر | 428 | +| `user.list.remove` | حذف مدیر | 432 | +| `user.list.create` | افزودن مدیر | 436 | +| `user.permission` | دسترسی ها | 442 | +| `user.permission.edit` | ویرایش دسترسی | 447 | +| `user.permission.create` | افزودن دسترسی | 451 | +| `collection` | دسته بندی ها | 459 | +| `collection.create` | افزودن دسته بندی | 464 | +| `collection.list` | لیست دسته بندی ها | 468 | +| `collection.list.view` | مشاهده لیست دسته بندی ها | 473 | +| `collection.list.edit` | ویرایش دسته | 477 | +| `collection.list.remove` | حذف دسته | 481 | +| `document` | اسناد مالی | 489 | +| `document.list` | لیست اسناد مالی | 494 | +| `document.list.view` | مشاهده لیست اسناد مالی | 499 | +| `document.list.edit` | ویرایش سند مالی | 503 | +| `document.list.remove` | حذف سند مالی | 507 | +| `document.list.deny` | رد کردن سند مالی | 511 | +| `document.list.confirm` | تأیید کردن سند مالی | 515 | +| `document.list.get` | دریافت pdf سند مالی | 519 | +| `document.create` | وارد کردن سند مالی مشتری | 525 | +| `offersIgnore` | استثناعات تخفیف | 531 | +| `offers` | تخفیفات | 535 | +| `offers.list` | لیست تخفیفات | 540 | +| `offers.list.view` | مشاهده لیست تخفیفات | 545 | +| `offers.list.edit` | ویرایش تخفیف عمده | 549 | +| `offers.list.tak.edit` | ویرایش تخفیف تک | 553 | +| `offers.list.remove` | حذف تخفیف عمده | 557 | +| `offers.list.tak.remove` | حذف تخفیف تک | 561 | +| `offers.create` | تعریف تخفیف عمده | 567 | +| `offers.create.tak` | تعریف تخفیف تک | 571 | +| `offers.autopromotion` | تخفیف اتوماتیک | 575 | +| `invoice` | فاکتور انتقال | 581 | +| `invoice.create` | افزودن فاکتور | 586 | +| `invoice.list` | لیست فاکتورها | 590 | +| `report` | گزارشات | 596 | +| `report.order.item` | گزارش تجمیعی سفارشات و اسناد مالی | 601 | +| `report.product` | گزارش محصولات | 605 | +| `cities` | شهرها | 611 | +| `cities.create` | افزودن شهر | 616 | +| `cities.list` | لیست شهرها | 620 | +| `cities.list.view` | مشاهده لیست شهرها *(duplicate at 633: «مشاهده شهر»)* | 625, 633 | +| `cities.list.remove` | حذف شهر | 629 | +| `cities.list.view.edit` | ویرایش شهر | 637 | +| `comment` | دیدگاه ها | 645 | +| `comment.view.all` | مشاهده لیست دیدگاه ها | 649 | +| `comment.state` | تغییر وضعیت دیدگاه | 653 | +| `comment.view` | مشاهده دیدگاه | 657 | +| `comment.view.edit` | ویرایش دیدگاه | 661 | +| `comment.remove` | حذف دیدگاه | 665 | +| `sms` | پیامک | 671 | +| `sms.create` | ارسال پیامک | 676 | +| `sms.create.single` | ارسال برای یک شماره تلفن | 681 | +| `sms.create.all` | ارسال برای همه کاربران | 685 | +| `sms.create.reagent` | ارسال برای کارشناسان فروش | 689 | +| `sms.create.omde` | ارسال برای مشتریان عمده | 693 | +| `sms.create.multiple` | ارسال برای چند شماره تلفن | 697 | +| `sms.list` | لیست پیامک های ارسال شده | 703 | +| `wallet` | کیف پول | 710 | +| `wallet.list` | لیست کیف پول های شارژ شده | 715 | +| `wallet.charge` | شارژ کیف پول | 719 | +| `wallet.percent` | تغییر درصد شارژ کیف پول | 723 | +| `notify` | نوتیفیکیشن | 729 | +| `notify.create` | ارسال نوتیفیکیشن | 734 | +| `notify.create.multiple.reagent` | ارسال برای کارشناسان فروش | 739 | +| `notify.create.single.customer` | ارسال برای یک مشتری | 743 | +| `notify.create.multiple.omde` | ارسال برای مشتریان عمده | 747 | +| `notify.create.multiple.customer` | ارسال برای همه مشتریان | 751 | +| `notify.create.single.admin` | ارسال برای یک مدیر | 755 | +| `notify.create.multiple.admin` | ارسال برای همه مدیران | 759 | +| `notify.me` | لیست نوتیفیکیشن های دریافتی | 765 | +| `config` | پیکربندی | 771 | +| `config.statics` | مدیریت ثابت ها | 775 | +| `config.statics.brand` | برند ها | 780 | +| `config.statics.brand.create` | افزودن برند | 785 | +| `config.statics.brand.edit` | ویرایش برند | 789 | +| `config.statics.brand.remove` | حذف برند | 793 | +| `config.statics.tags` | برچسب ها | 799 | +| `config.statics.tags.create` | افزودن برچسب | 804 | +| `config.statics.tags.edit` | ویرایش برچسب | 808 | +| `config.statics.tags.remove` | حذف برچسب | 812 | +| `config.wallet` | مدیریت کیف پول | 820 | +| `config.report` | گزارش | 824 | +| `statics` | آمار | 830 | +| `newsletters.newsletters` | خبرنامه | 834 | +| `custom` | سفارشی | 838 | +| `custom.config.statics.slider` | اسلایدر موبایل | 842 | +| `custom.config.statics.tags.banner.ids` | برچسب بنر اول سایت | 846 | + +**Referenced in code but NOT in `permissionConst.js`** (would never be grantable through the UI tree): +`persons.create.customer.tak` (views/apps/customer/AddCustomer.vue:142), `persons.list.sellers.view` (views/customer/reagentList/ReagentList.vue:32). + +### 5.3 Button-level permission checks — every `$hasPermission(...)` call site + +| Permission | Call sites (`file:line`) | +|---|---| +| `cities.list.remove` | views/city/cityList.vue:25 | +| `cities.list.view` | views/city/cityList.vue:18, 36 | +| `cities.list.view.edit` | views/user/ListUser.vue:108; views/city/cityList.vue:40, 47, 58, 66, 79, 90 | +| `collection.list.edit` | views/apps/category/List.vue:16 | +| `collection.list.remove` | views/apps/category/List.vue:24 | +| `comment.remove` | views/customer/Comments/Comments.vue:96 | +| `comment.state` | views/customer/Comments/Comments.vue:25 | +| `comment.view` | views/customer/Comments/Comments.vue:92, 104 | +| `comment.view.edit` | views/customer/Comments/Comments.vue:132, 155, 163 | +| `config.statics` | views/manage/Const.vue:3 | +| `config.statics.brand` | views/manage/Const.vue:8, 44 | +| `config.statics.brand.create` | views/manage/Const.vue:26, 36, 62, 72 | +| `config.statics.brand.edit` | views/manage/Const.vue:324, 342, 391, 411 | +| `config.statics.brand.remove` | views/manage/Const.vue:331 | +| `config.statics.tags` | views/manage/Const.vue:81 | +| `config.statics.tags.create` | views/manage/Const.vue:94, 104 | +| `config.statics.tags.edit` | views/manage/Const.vue:358, 375, 421 | +| `config.statics.tags.remove` | views/manage/Const.vue:365 | +| `config.wallet` | views/manage/wallet.vue:3 | +| `custom.config.statics.slider` | views/manage/Const.vue:122, 195 | +| `custom.config.statics.tags.banner.ids` | views/manage/Const.vue:110 | +| `document.list.confirm` | views/finical/list.vue:12 | +| `document.list.deny` | views/finical/list.vue:30 | +| `document.list.edit` | views/finical/list.vue:48 | +| `document.list.remove` | views/finical/list.vue:58 | +| `document.list.get` | views/finical/list.vue:76 | +| `notify` | layouts/components/navbar/TheNavbarVertical.vue:16 | +| `offers.list.edit` / `offers.list.tak.edit` | views/promotion/promotion_list.vue:53 | +| `offers.list.remove` / `offers.list.tak.remove` | views/promotion/promotion_list.vue:62 | +| `order.create` | views/invoice/EditInvoice.vue:3; views/invoice/AddInvoice.vue:3; views/order/create.vue:2 | +| `order.list.pdf` | views/order/List.vue:16 | +| `order.list.sts` | views/order/List.vue:41, 47; views/branch/buyList.vue:37, 43; views/workshop/buyList.vue:37, 43 | +| `order.list.sts.remove` | views/order/List.vue:60; views/branch/buyList.vue:56; views/workshop/buyList.vue:56 | +| `order.list.sts.submit` | views/order/List.vue:72; views/branch/buyList.vue:68; views/workshop/buyList.vue:68 | +| `order.list.view` | views/order/List.vue:13, 90 | +| `order.list.view.edit` | views/order/List.vue:135, 172, 182, 186, 205; views/invoice/ListInvoice.vue:125; views/branch/buyList.vue:98, 139; views/workshop/buyList.vue:98, 139 | +| `order.makeOrder.get` | views/order/MakeOrder.vue:21 | +| `order.makeOrder.remove` | views/order/MakeOrder.vue:18 | +| `order.makeOrder.state` | views/order/MakeOrder.vue:34, 42 | +| `order.makeOrder.state.remove` | views/order/MakeOrder.vue:54 | +| `order.makeOrder.state.submit` | views/order/MakeOrder.vue:65 | +| `order.makeOrder.view` | views/order/MakeOrder.vue:16, 82 | +| `order.makeOrder.view.edit` | views/order/MakeOrder.vue:108, 134, 139, 144, 149, 191 | +| `order.omde.get` | views/order/OmdeList.vue:21 | +| `order.omde.remove` | views/order/OmdeList.vue:18 | +| `order.omde.state` | views/order/OmdeList.vue:34, 42 | +| `order.omde.state.remove` | views/order/OmdeList.vue:54 | +| `order.omde.state.submit` | views/order/OmdeList.vue:65 | +| `order.omde.view` | views/order/OmdeList.vue:16, 82 | +| `order.omde.view.edit` | views/order/OmdeList.vue:107, 134, 139, 152, 159, 160 | +| `order.omde.view.remove` | views/order/OmdeList.vue:161 | +| `order.tamin.state` | views/order/TaminList.vue:23, 32 | +| `order.tamin.state.remove` | views/order/TaminList.vue:44 | +| `order.tamin.state.submit` | views/order/TaminList.vue:55 | +| `order.tamin.view` | views/order/TaminList.vue:12, 72 | +| `order.tamin.view.edit` | views/order/TaminList.vue:114, 129, 130 | +| `persons.create` | views/apps/customer/AddCustomer.vue:5 | +| `persons.create.client` | views/apps/customer/AddCustomer.vue:6 | +| `persons.create.seller` | views/apps/customer/AddCustomer.vue:137 | +| `persons.create.customer.tak` | views/apps/customer/AddCustomer.vue:142 *(commented out — tab always shown, line 143)* | +| `persons.list.client.omde.able` | OmdeList.vue:50,119,170; OmdeNonVerifiedList.vue:50,119,168; OmdeArchivedList.vue:50,119,168; ShopkeeperList.vue:50,119,170; BankerList.vue:50,121,172 | +| `persons.list.client.omde.able.order` | OmdeList.vue:108,158; OmdeNonVerifiedList.vue:108,156; OmdeArchivedList.vue:108,156; ShopkeeperList.vue:108,158; BankerList.vue:109,160 | +| `persons.list.client.omde.edit` | OmdeList.vue:22,139; OmdeNonVerifiedList.vue:22,137; OmdeArchivedList.vue:22,137; ShopkeeperList.vue:22,139; BankerList.vue:22,141 | +| `persons.list.client.omde.unable` | OmdeList.vue:165; OmdeNonVerifiedList.vue:163; OmdeArchivedList.vue:163; ShopkeeperList.vue:165; BankerList.vue:167 | +| `persons.list.client.omde.unable.order` | OmdeList.vue:99,153; OmdeNonVerifiedList.vue:99,151; OmdeArchivedList.vue:99,151; ShopkeeperList.vue:99,153; BankerList.vue:99,155 | +| `persons.list.client.omde.view` | OmdeList.vue:29,142; OmdeNonVerifiedList.vue:29,140; OmdeArchivedList.vue:29,140; ShopkeeperList.vue:29,142; BankerList.vue:29,144 | +| `persons.list.client.omde.view.documents` | OmdeList.vue:43,148; OmdeNonVerifiedList.vue:43,146; OmdeArchivedList.vue:43,146; ShopkeeperList.vue:43,148; BankerList.vue:43,150 | +| `persons.list.client.omde.view.orders` | OmdeList.vue:36,145; OmdeNonVerifiedList.vue:36,143; OmdeArchivedList.vue:36,143; ShopkeeperList.vue:36,145; BankerList.vue:36,147 | +| `persons.list.sellers.able` | views/customer/reagentList/ReagentList.vue:61 | +| `persons.list.sellers.able.order` | ReagentList.vue:49 | +| `persons.list.sellers.edit` | ReagentList.vue:20, 29 | +| `persons.list.sellers.unable` | ReagentList.vue:54 | +| `persons.list.sellers.unable.order` | ReagentList.vue:44 | +| `persons.list.sellers.view` | ReagentList.vue:32 | +| `persons.list.sellers.view.documents` | ReagentList.vue:40 | +| `persons.list.sellers.view.orders` | ReagentList.vue:36 | +| `product.list.edit` | views/apps/product/ProductList.vue:24; views/report/product.vue:14 | +| `product.list.group.edit` | views/apps/product/ProductList.vue:108 | +| `product.list.remove` | views/apps/product/ProductList.vue:54 | +| `product.list.variants` | ProductList.vue:44, 181; AddProduct.vue:274 | +| `product.list.variants.add` | ProductList.vue:190; AddProduct.vue:282 | +| `product.list.variants.edit` | ProductList.vue:213; AddProduct.vue:297 | +| `product.list.variants.remove` | ProductList.vue:221; AddProduct.vue:306 | +| `product.list.view` | ProductList.vue:34; views/report/product.vue:23 | +| `sms.create` | views/sms/create.vue:57 | +| `sms.create.all` | views/sms/create.vue:18 | +| `sms.create.multiple` | views/sms/create.vue:27 | +| `sms.create.omde` | views/sms/create.vue:24 | +| `sms.create.reagent` | views/sms/create.vue:21 | +| `sms.create.single` | views/sms/create.vue:15 | +| `sms.list` | views/sms/create.vue:61, 64 | +| `user.list.create` | views/user/ListUser.vue:4 | +| `user.list.edit` | views/user/ListUser.vue:10, 41 | +| `user.list.remove` | views/user/ListUser.vue:20 | +| `user.permission.create` | views/user/Permission.vue:16, 36, 115 | +| `user.permission.edit` | views/user/Permission.vue:34 | +| `wallet.charge` | views/customer/single-customer/SingleCustomer.vue:18, 26, 37 | +| `wallet.list` | views/wallet/wallet.vue:3 | +| `wallet.percent` | views/wallet/wallet.vue:29 | + +### 5.4 Permission checks inside table models (column visibility) + +| Model file | Permission | Effect | +|---|---|---| +| views/order/orderListModel.js:14-17 | `order.tamin.state` | adds `sts_str` + `branch.name` columns | +| views/order/OmdeListModel.js:20-32 | `order.omde.state` | adds `sts` column with the 7-value status filter | +| views/order/TaminListModel.js:14-16 | `order.tamin.state` | adds `sts` column | +| views/order/makeOrderModel.js:19-22 | `order.tamin.state` | adds `sts_str_fa` column | +| views/customer/Comments/CommentListModel.js:12-14 | *(role check)* | adds `changeState` column | + +--- + +## 6. Views + +### 6.1 Full file listing under `src/views` (business + template) + +Legend: **[B]** = business (reachable from a live route or embedded in one), **[T]** = Vuexy template demo / dead. + +#### Root +| File | Purpose | +|---|---| +| `views/Callback.vue` | [T] Auth0 callback stub (auth0 disabled at main.js:36-38). | +| `views/DashboardAnalytics.vue` | [B] Single-retail (تک) dashboard; route `/dashboard/analytics`, role `dashboard.tak`. | +| `views/DashboardECommerce.vue` | [B] Wholesale (عمده) dashboard; route `/dashboard/ecommerce`, role `dashboard.omde`. | +| `views/Inp.vue` | [T] input scratch page, unrouted. | +| `views/reg.vue` | [B] Public registration page; route `/reg`, full-page layout. | + +#### `views/auth` +| File | Purpose | +|---|---| +| `views/auth/Login.vue` | [B] Admin login form (mobile + password). | + +#### `views/user` +| File | Purpose | +|---|---| +| `views/user/ListUser.vue` | [B] Admin (مدیر) list + inline edit modal + delete. | +| `views/user/UserCreate.vue` | [B] Create/edit admin form. | +| `views/user/UserEdit.vue` | [B] 14-line wrapper that renders `UserCreate` in edit mode. | +| `views/user/Permission.vue` | [B] Role (`ut`) ⇄ permission-tree editor. | +| `views/user/myData.vue` | [B] 33-line "my profile" landing (default redirect target of `/`). | +| `views/user/permissionConst.js` | [B] The 172-key permission tree (see §5.2). | + +#### `views/customer` +| File | Purpose | +|---|---| +| `views/customer/omde/OmdeList.vue` | [B] Wholesale customer list (614 lines) — the richest list view. | +| `views/customer/omde/OmdeNonVerifiedList.vue` | [B] Pending-verification wholesale customers (612 lines). | +| `views/customer/omde/OmdeArchivedList.vue` | [B] Archived wholesale customers (612 lines). | +| `views/customer/omde/BankerList.vue` | [B] Bankers (بنکدار) list (620 lines). | +| `views/customer/omde/ShopkeeperList.vue` | [B] In-person shopkeepers list (614 lines). | +| `views/customer/omde/ViewOmdeCustomer.vue` | [B] Wholesale customer 360° detail page (376 lines): orders/documents/sms/transactions tabs. | +| `views/customer/omde/OmdeAdd.vue` | [B] Wholesale add form (215 lines, embedded). | +| `views/customer/omde/{OmdeListModel,OmdeListNonVerifiedModel,OmdeArchiveListModel,BankerListModel,shopkeeperListModel}.js` | [B] XTbl column models for the five lists above. | +| `views/customer/omde/{omdeCustomer,omdeOrder,omdeSms,omdeTransaction}.js` | [B] XTbl models used inside `ViewOmdeCustomer` tabs. | +| `views/customer/single-customer/SingleCustomer.vue` | [B] Retail customer list (142 lines) + wallet-charge actions. | +| `views/customer/single-customer/ViewSingleCustomer.vue` | [B] Retail customer detail (425 lines). | +| `views/customer/single-customer/SingleCustomerList.js` | [B] XTbl model. | +| `views/customer/single-customer/{singleOrder,singleSms,singleTransaction}.js` | [B] XTbl models for detail tabs. | +| `views/customer/reagentList/ReagentList.vue` | [B] Sales-expert (کارشناس فروش) list. | +| `views/customer/reagentList/ReagentListModel.js` | [B] XTbl model. | +| `views/customer/Comments/Comments.vue` | [B] Product-comment moderation. | +| `views/customer/Comments/CommentListModel.js` | [B] XTbl model. | +| `views/customer/{List,CustomerEdit,CustomerEditTab*}.vue`, `views/customer/cell-renderer/*` | [T] ag-Grid-era leftovers, unrouted. | + +#### `views/apps/customer` +| File | Purpose | +|---|---| +| `views/apps/customer/AddCustomer.vue` | [B] 3-tab person creation shell (wholesale / sales-expert / retail). | +| `views/apps/customer/bulkCustomer.vue` | [B] Wholesale customer form (385 lines) — embedded + edit mode. | +| `views/apps/customer/ReagentCustomer.vue` | [B] Sales-expert form (304 lines). | +| `views/apps/customer/singleCustomer.vue` | [B] Retail customer form (131 lines). | +| `views/apps/customer/EditBulkCustomer.vue` | [B] 19-line wrapper → `bulkCustomer` with `:id`. | +| `views/apps/customer/EditReagent.vue` | [B] 19-line wrapper → `ReagentCustomer` with `:id`. | +| `views/apps/customer/EditSingleCustomer.vue` | [B] 16-line wrapper → `singleCustomer` with `:id`. | +| `views/apps/customer/{Edit,CustomerView}.vue`, `customer-edit/*` | [T] unrouted template leftovers. | + +#### `views/apps/product` +| File | Purpose | +|---|---| +| `views/apps/product/AddProduct.vue` | [B] Product create form + inline variant manager (1044 lines — largest view). | +| `views/apps/product/EditProduct.vue` | [B] 71-line wrapper → `AddProduct` with `:id`/`:variant_id`. | +| `views/apps/product/ProductList.vue` | [B] Product list + variants drawer + bulk category change (694 lines). | +| `views/apps/product/ChangePercent.vue` | [B] Bulk wage-percent change tool (254 lines). | +| `views/apps/product/Varaint.vue` | [B] Variant editor component (396 lines) — embedded. | +| `views/apps/product/EditVariant.vue` | [B] 15-line wrapper. | +| `views/apps/product/comp/ProductUploader.vue`, `uploadMixin.js` | [B] Product image upload widget + mixin. | + +#### `views/apps/category` +| File | Purpose | +|---|---| +| `views/apps/category/Add.vue` | [B] Category create/edit form (422 lines). | +| `views/apps/category/Edit.vue` | [B] 12-line wrapper. | +| `views/apps/category/List.vue` | [B] Category list (313 lines). | +| `views/apps/category/CategoryTbl.js` | [B] XTbl model. | + +#### `views/order` +| File | Purpose | +|---|---| +| `views/order/List.vue` | [B] Retail order list + status timeline modal + order-detail modal (322 lines). | +| `views/order/OmdeList.vue` | [B] Wholesale order list (324 lines) — same structure + item removal. | +| `views/order/TaminList.vue` | [B] Supply (تامین) list (241 lines). | +| `views/order/MakeOrder.vue` | [B] Made-to-order (ساختنی) list (432 lines). | +| `views/order/create.vue` | [B] Retail order creation with cart (451 lines). | +| `views/order/Add.vue` | [T] 170-line older add form, unrouted. | +| `views/order/{orderListModel,OmdeListModel,TaminListModel,makeOrderModel,product,workshopModel}.js` | [B] XTbl models. | + +#### `views/finical` +| File | Purpose | +|---|---| +| `views/finical/addFinDoc.vue` | [B] Financial-document create/edit; 4 doc types (382 lines). | +| `views/finical/list.vue` | [B] Financial-document list with confirm/deny/pdf (166 lines). | +| `views/finical/list_model.js` | [B] XTbl model. | + +#### `views/promotion` & `views/offersIgnore` +| File | Purpose | +|---|---| +| `views/promotion/promotion_list.vue` | [B] Promotion list (151 lines). | +| `views/promotion/promotion_add_edit.vue` | [B] Wholesale promotion create/edit (415 lines). | +| `views/promotion/promotion_tak_add_edit.vue` | [B] Retail promotion create/edit (388 lines). | +| `views/promotion/autopromotion.vue` | [B] Automatic-discount config (92 lines). | +| `views/offersIgnore/offersIgnore-add-edit.vue` | [B] Discount-exception editor (259 lines). | + +#### `views/branch` +| File | Purpose | +|---|---| +| `views/branch/AddBranches.vue` | [B] Branch create (108 lines). | +| `views/branch/EditBranches.vue` | [B] Branch edit (112 lines). | +| `views/branch/ListBranches.vue` | [B] Branch list (88 lines). | +| `views/branch/ListBranch.js` | [B] XTbl model. | +| `views/branch/dashboard.vue` | [B] Branch-build dashboard (186 lines). | +| `views/branch/login.vue` | [B] Branch-build login (82 lines). | +| `views/branch/addCustomer.vue` | [B] 19-line branch customer wrapper. | +| `views/branch/buyList.vue` | [B] Branch purchase-invoice list (262 lines). | +| `views/branch/order/create.vue` | [B] Branch order creation. | +| `views/branch/orderListModel.js` | [B] XTbl model. | + +#### `views/workshop` +| File | Purpose | +|---|---| +| `views/workshop/AddWorkshop.vue` | [B] Workshop create (117 lines). | +| `views/workshop/EditWorkshop.vue` | [B] Workshop edit (122 lines). | +| `views/workshop/ListWorkshop.vue` | [B] Workshop list (88 lines). | +| `views/workshop/ListWorkshop.js`, `orderListModel.js` | [B] XTbl models. | +| `views/workshop/{addCustomer,buyList,login}.vue` | [B/T] copies of the branch equivalents; only reachable via the workshop feature. | + +#### `views/invoice` +| File | Purpose | +|---|---| +| `views/invoice/AddInvoice.vue` | [B] Transfer-invoice create with product picker (322 lines). | +| `views/invoice/EditInvoice.vue` | [B] Transfer-invoice edit (333 lines). | +| `views/invoice/ListInvoice.vue` | [B] Transfer-invoice list (217 lines). | +| `views/invoice/{AddInvoice,ListInvoice}.js` | [B] XTbl models. | + +#### `views/city` +| File | Purpose | +|---|---| +| `views/city/createCity.vue` | [B] City create (166 lines). | +| `views/city/cityList.vue` | [B] City list with inline edit + percent-aux modal (303 lines). | +| `views/city/components/form.vue` | [B] Shared city form fields. | +| `views/city/CityListModel.js` | [B] XTbl model. | + +#### `views/manage`, `views/statics`, `views/report`, `views/wallet`, `views/sms`, `views/notify`, `views/newsletters` +| File | Purpose | +|---|---| +| `views/manage/Const.vue` | [B] Constants manager: brands, tags, mobile slider, site banner (977 lines). | +| `views/manage/Const_new.vue` | [T] 256-line WIP rewrite, unrouted. | +| `views/manage/wallet.vue` | [B] Wallet config: birthday credit + per-order charge percent (142 lines). | +| `views/manage/report.vue` | [B] Metabase/dashboard-link embed (65 lines). | +| `views/statics/index.vue` | [B] Product-view analytics by path/customer (207 lines). | +| `views/report/order_item.vue` | [B] Consolidated order-item + document report (133 lines). | +| `views/report/product.vue` | [B] Product/variant report (111 lines). | +| `views/report/order_item_model.js` | [B] XTbl model. | +| `views/wallet/wallet.vue` | [B] Charged-wallet list + percent config (103 lines). | +| `views/wallet/walletTbl.js` | [B] XTbl model. | +| `views/sms/create.vue` | [B] SMS blast composer (265 lines). | +| `views/sms/list.vue` | [B] Sent-SMS list (115 lines) — not separately routed; used by `create.vue`. | +| `views/notify/create.vue` | [B] Push-notification composer (156 lines). | +| `views/notify/user.vue` | [B] Received-notification list (110 lines). | +| `views/notify/notifyTbl.js` | [B] XTbl model. | +| `views/newsletters/newsletters.vue` | [B] Newsletter subscriber list (59 lines). | +| `views/newsletters/newsletter.js` | [B] XTbl model. | + +#### `views/apps` (template / partial) +| File | Purpose | +|---|---| +| `views/apps/finance/{BagList,DebitList,FeeList,OrderList}.vue` | [T] earlier finance lists, unrouted. | +| `views/apps/logs/{LogsList,Rules}.vue` | [T] unrouted. | +| `views/apps/promotions/{AddPromotion,Editpromotion,PromotionList,VariantPromotion}.vue` | [T] superseded by `views/promotion/*`, unrouted. | +| `views/apps/ticket/*` (5 files) | [T] unrouted. | +| `views/apps/user/*` (9 files) | [T] Vuexy user-management demo, unrouted. | +| `views/apps/{calendar,chat,email,todo,eCommerce}/*` | [T] Vuexy demo apps, unrouted. | + +#### Pure template demos (unrouted, not enumerated individually) +`views/components/**` (~120 files), `views/charts-and-maps/**` (~40), `views/forms/**` (~50), `views/ui-elements/**`, `views/pages/{Error404,Error500,NotAuthorized,Maintenance}.vue` (routed — see §3.5). + +**Business-view count: ~95 files** (`.vue` + XTbl `.js` models) under the directories listed above. + +--- + +### 6.2 Significant views in detail + +#### `views/order/List.vue` — Retail order list (322 lines) + +| Aspect | Detail | +|---|---| +| Table model | `views/order/orderListModel.js` → `POST https://henzagold.com/oapi/admin/single-order` (orderListModel.js:4) | +| Title | `is_branch ? 'لیست سفارش' : 'لیست سفارش های تک فروشی'` (List.vue:6, 238-240) | +| Columns | `id` (شماره, filterNumber), `customer.fname` (نام مشتری, text), `customer.lname` (نام خانوادگی, text), `total_price` (جمع کل سفارش, text, slot `total_price` → `| priceUnit`), `items` (تعداد آیتم ها, slot `items_len` = `text.length`, noSort), `caj` (تاریخ ثبت, filterDate, noSort), `uaj` (تاریخ ویرایش, **hidden**, filterDate, noSort), `user` (ثبت کننده, slot, noSort), `opt` (عملیات, slot, noSort) — orderListModel.js:5-19 | +| Conditional columns | if role `order.tamin.state`: `sts_str` (وضعیت) + `branch.name` (نام شعبه) — orderListModel.js:14-17 | +| Row actions | مشاهده سفارش (`ListIcon`, gate `order.list.view`, opens detail modal) — List.vue:13-15; دانلود pdf (`FileIcon`, gate `order.list.pdf`) → `window.open('https://henzagold.com/oapi/admin/order/pdf/{id}','_blank')` — List.vue:16-18, 295-297 | +| Modal 1: status timeline | gate `order.list.sts`; `ATimeline` over `tempRow.activity`; color `green` if `item.proceed` else `gray` (List.vue:246-248). Per step: submit → `POST single/order-sts/edit` with the item (gate `order.list.sts.submit`, List.vue:72-78, 258-267); remove → same endpoint with `row.remove = true` (gate `order.list.sts.remove`, index > 0 only, List.vue:60-68, 268-278). Both close the modal, `$message.info('ویرایش موفق آمیز')` / `$message.error('خطا در ویرایش')`, then `this.$refs.tbl.fetch()`. Confirmation uses `APopconfirm` with title «آیا مطمئن به حذف وضعیت مورد نظر هستید ؟», ok `بله`, cancel `بی خیال`. `showSts` is a **no-op when `row.branch_id` is set** (List.vue:249-254). | +| Modal 2: order detail | gate `order.list.view`, width `80%`. `ADescriptions`: مشتری, تلفن همراه, تاریخ ثبت, وضعیت, نام دریافت کننده محصول, تلفن همراه دریافت کننده, تلفن ثابت دریافت کننده (List.vue:93-115); آدرس ارسال block: استان, شهرستان, کد پستی, آدرس (List.vue:116-129). Items card: thumbnail via `ZoomableImage` (falls back to `assets/images/no-image.png`), نام محصول (`ATag` blue), عنوان تنوع, وزن نهایی (`AInputNumber` when `editMode && order.list.view.edit`, else red `ATag`), تعداد (editable likewise), توضیحات (List.vue:140-197). Footer: `باشه` + `ویرایش سفارش` (gate `order.list.view.edit`). **`editRow()` has its `axios.put('order/'+id, …)` commented out** (List.vue:279-289) — it only toggles state and refetches. | +| Export | inherited from XTable toolbar (excel / json / print). | + +#### `views/order/OmdeList.vue` / `TaminList.vue` / `MakeOrder.vue` + +Same three-part shape (list + status timeline + detail modal), differing in endpoint, permission family and status vocabulary: + +| View | Table URL | Permission family | Status endpoint | +|---|---|---|---| +| `OmdeList.vue` | `POST .../major/order/table` (OmdeListModel.js:7) | `order.omde.*` | `POST major/order-sts/edit` | +| `TaminList.vue` | `POST .../order-tamin` (TaminListModel.js:3) | `order.tamin.*` | `POST order-sts/edit` | +| `MakeOrder.vue` | `POST .../get-custom-orders` (makeOrderModel.js:4) | `order.makeOrder.*` | delete via `DELETE custom-order/{id}`, edit via `PUT custom-order/{id}` | + +`OmdeList` adds row-level delete (`order.omde.remove`), pdf (`order.omde.get`), and per-item removal inside the detail modal (`order.omde.view.remove` → `DELETE major/order_item/`). + +Wholesale order status filter (`OmdeListModel.js:21-32`): +`0: معلق`, `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی `, `5: تحویل مسئول فروش`, `6: تحویل مشتری `. +Customer-confirmation filter (OmdeListModel.js:33-38, TaminListModel.js:17-22): `1: تایید شده`, `2: در انتظار تایید`. + +`MakeOrder` columns (makeOrderModel.js:5-24): `customer.id` (شماره), `customer.fname`, `customer.lname`, `color` (رنگ محصول), `size` (سایز محصول), `color_mina` (رنگ مینا), `color_stone` (رنگ سنگ), `sum` (تعداد), `time_creat_fa` (تاریخ ثبت, filterDate), `time_update_fa` (hidden), `customer.name` (ثبت کننده), `sts_str_fa` (وضعیت, only with `order.tamin.state`), `opt`. Admin-confirm sets `sts_str_fa = "تایید ادمین"` (MakeOrder.vue:397). + +#### `views/order/create.vue` — Retail order creation (451 lines) + +| Aspect | Detail | +|---|---| +| Page gate | `$hasPermission('order.create')` on the root element (create.vue:2) | +| Cart | vuex `cart` module: `cart/add` (create.vue:229), `cart/remove` (create.vue:32), `cart/updateItemQuantity` (create.vue:44), `cart/clear` (create.vue:276), `cart/init` (create.vue:307, 399) | +| Qty control | `AInputNumber` min `1`, max `variant.qty > 0 ? variant.qty : Infinity` (create.vue:44) | +| Bootstrap calls | `GET /order/type` → sale types (create.vue:395); `POST /single-customer` → `rows` as the customer dropdown (create.vue:397); `cart/init` (create.vue:399) | +| Fields (all ant `v-decorator`) | `customer_id` — **required**, «مقدار را وارد نمایید», searchable select (create.vue:77); `sts` — **required** (create.vue:84); `type` — **required** (create.vue:91); `shipping_address.reciver_fname`, `.reciver_lname`, `.province`, `.city`, `.zipcode`, `.reciver_phone`, `.reciver_fixed_phone` — all optional, same message (create.vue:98-116); `note` textarea `{minRows:3,maxRows:5}` optional (create.vue:120); `shipping_address.address` textarea optional (create.vue:123); hidden `app` initialValue `'app'` (create.vue:126); hidden `reagent_id` initialValue from data (create.vue:129) | +| `sts` enum (create.vue:158-165) | `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی`, `5: ارسال`, `6: تحویل مشتری` — **note: no `0: معلق` here, unlike the wholesale list filter** | +| Inline variant creation | `POST add/variant` with `{...variant, title: variant.grams + ' گرم'}` (create.vue:304) — the variant title is auto-derived from the weight | +| Product lookup | `GET product/show/{id}` (create.vue:360); `PUT product/update/{id}` (create.vue:341) | + +#### `views/apps/product/AddProduct.vue` — Product create/edit (1044 lines) + +Fields (`a-form-item` label → `v-decorator` key → rules): + +| Label | Field | Rules | Line | +|---|---|---|---| +| کد محصول | `product_code` | **required**, «لطفا مقدار را وارد کنید» | 11-14 | +| نام محصول | `title` | **required**, same msg | 16-19 | +| نوع نمایش | `show_type` (radio) | **required**, initialValue `3` — `1: سایت`, `2: اپلیکشن موبایل`, `3: سایت و اپلیکشن موبایل` | 21-35 | +| دسته بندی | `cats` (multiple select) | **required**, «وارد کردن دسته بندی الزامی است» | 37-52 | +| برند | `brand_id` | **required** | 53-68 | +| سینی ها | `ciny_ids` | rules commented out (optional) | 69-99 | +| برچسب ها | `tags` | optional, «وارد کردن برچسب الزامی است » | 100-115 | +| شرح محصول (موبایل) | `body_html` | none (quill editor) | 116-121 | +| شرح محصول (سایت) | *(separate editor)* | none | 122-129 | +| نوع تحویل | `delivery_type` (radio) | **required**, initialValue `1` — `1: کوتاه مدت`, `2: بلند مدت` | 130-142 | +| متن پیام نمایش به کاربر در بازه بلند مدت | `message_long_delivery` | none; `disabled` unless `delivery_type == 2` | 143-149 | +| نمایش محصول | `is_active` (switch) | initialValue `true`, `valuePropName: 'checked'` | 150-152 | +| پیشنهاد روز | `offer_day` (switch) | initialValue `true` | 153-155 | +| تضمین قیمت | `has_warranty` (switch) | initialValue `true` | 156-158 | +| موجود | `is_exist` (switch) | initialValue `true` | 159-161 | + +Variant sub-form (inside the same page, gated by `product.list.variants`, AddProduct.vue:274): + +| Label | Field | Rules | Line | +|---|---|---|---| +| عنوان | `title` | **required** (empty msg) | 345-352 | +| تعداد | `qty` | **required** | 353-363 | +| وزن | `grams` | **required** | 364-371 | +| رنگ | `color` | **required**, «لطفا مقدار را وارد کنید»; options from `GET /product/color` (AddProduct.vue:857) | 372-380 | +| فعال | `is_active` (switch) | — | 381-383 | +| پیش سفارش | `is_pre_order` (switch) | — | 384-386 | +| اجرت ریالی | `rial_pay` | **required** | 391-399 | +| اجرت درصدی | `percentage_pay` | **required** | 400-409 | +| توضیح مختصر | `details` | none | 411-413 | +| توضیحات کامل | `description` | none | 414-416 | +| *(hidden)* | `product_id_id`, `id` | none | 417-418 | + +Variant actions gated: add `product.list.variants.add` (282), edit `product.list.variants.edit` (297), remove `product.list.variants.remove` (306). + +API calls: `POST product/create` (660); `PUT product/update/{id}` (698); `GET product/show/{id}` (718); `GET /product/color` (857); `POST category` (976); `GET brand?text_search=%` (986); `GET /const/__list__tags` (989); `GET https://henzagold.com/oapi/admin/ciny` (991). + +#### `views/apps/product/ProductList.vue` (694 lines) + +| Aspect | Detail | +|---|---| +| Model | `src/table/product.js:20` → `POST .../products`, title «لیست محصولات» | +| Columns | `id` (کد, filterNumber), `ProductImage` (تصویر محصول, slot `image`, noSort), `title` (نام محصول, text), `brand.title` (برند, text), `category.collection.title` (دسته بندی, text), `product_code` (کد محصول, text), `show_type` (نوع نمایش, filterSelect), `action` (عملیات, slot `op`, noSort) — table/product.js:21-32 | +| Row actions | ویرایش (`product.list.edit`, line 24) → `window.open('/admin/apps/product/edit/{id}','_blank')` (648); مشاهده (`product.list.view`, 34); تنوع ها (`product.list.variants`, 44) → `POST get/variants {id}` (631); حذف (`product.list.remove`, 54) → `POST delete/product/{id}` then `$refs.tblProducts.fetch()` (670-672) | +| Variant drawer actions | add → `window.open('/admin/apps/product/{id}/variant/add','_blank')` (546); toggles → `GET product/warranty/{id}` (556), `GET product/exist/{id}` (558); offer toggle → `GET /product/offer/{id}` (570); variant reload → `GET product/{product_id}/variant` (665) | +| Bulk edit | multi-select rows → card «تغییر دسته ای» (gate `product.list.group.edit`, line 108-114) with confirm «آیا مطمئن به تغییر {n} محصول هستید ؟» → `POST /change/all` (582) | + +#### `views/apps/customer/*` — person forms + +All three tabs share the ant `v-decorator` pattern; **every rule is `required: false`** — validation is effectively server-side only. + +**`bulkCustomer.vue`** (wholesale, 385 lines) fields: `fname` (نام, 17), `lname` (نام خانوادگی, 24), `ncode` (کد ملی, 31), `password` (رمز عبور, 40), `mobile` (شماره تلفن همراه, 48) with pattern `/^(?:0|98|\+98|\+980|0098|098|00980)?(9\d{9})$/` msg «تلفن وارد شده نامعتبر است», `type` (نوع کاربری, 61), `store_name` (نام مغازه, 71), `phone` (تلفن, 78), `pasazh_name` (نام پاساژ, 85), `city_id` (شهر, 94, msg «شهر را انتخاب نمایید»), `mcode` (کد مشتری ته حساب, 106), a Jalali date field (116, msg «تاریخ دریافت را وارد نمایید»), image uploads عکس جواز کسب / عکس سردر مغازه / عکس پروفایل (119-143), `address` (آدرس, 147), `reagent_id` (کارشناس فروش, 164/182, msg «کارشناس فروش را انتخاب نمایید»), `percent` (درصد روی محصول, 197, msg «درصد روی محصول را وارد نمایید»), `gram_level` (سقف اعتبار, 204, msg «سقف اعتبار را وارد نمایید»). +Calls: `GET cities/get-percent-aux/{city_id}` on city change (265); `dispatch('customer/omdeSave', values)` (296); `GET query/reagent` (315); `POST cities/list` (321); `GET customer/{id}` in edit mode (330). + +**`ReagentCustomer.vue`** (304 lines) — same field set minus wholesale-only fields; mobile pattern differs: `/^09(0[1-2]|1[0-9]|3[0-9]|2[0-1])-?[0-9]{3}-?[0-9]{4}$/` (line 52). Calls `GET cities/get-percent-aux/{v}` (221), `dispatch('customer/omdeSave')` (245), `POST cities/list` (265), `GET customer/{id}` (274). + +**`singleCustomer.vue`** (retail, 131 lines) fields: `fname` (9), `lname` (16), `phone` (24), Jalali date (34), `city_id` (40). Calls: `PUT single/{id}` on edit (90), `POST single` on create (92), `POST cities/list` (110), `GET single/{id}` (119). + +#### `views/finical/addFinDoc.vue` — Financial documents (382 lines) + +Document type (`type`, initialValue `2`, addFinDoc.vue:18-32): + +| value | label | +|---|---| +| `1` | آب شده (melted gold) | +| `2` | حواله (transfer) | +| `3` | چک (cheque) | +| `4` | پولی (cash) | + +Fields shown conditionally on `form.getFieldValue('type')`: + +| Label | Field | Rule | Applies to | Line | +|---|---|---|---|---| +| نوع سند | `type` | optional, initial `2` | all | 18 | +| مشتری | `customer_id` | **required** | all | 39/46 | +| توضیحات | `note` | optional | all | 53 | +| وزن (گرم) | `original_value` | **required** | آب شده | 70 | +| عیار | `ayar` | **required** | آب شده | 76 | +| نام آزمایشگاه | `lab` | optional | آب شده | 89 | +| انگ | `ang` | optional | آب شده | 95 | +| وزن (گرم) | `value` | **required** | حواله | 110 | +| حواله به | `to` | optional, initialValue `'حواله کوثر'`; options = `'حواله کوثر'` + `havale_list` | حواله | 121 | +| مبلغ چک | `price` | **required** | چک | 138 | +| وزن (گرم) | `value` | optional | چک | 144 | +| نام صاحب حساب | `account_name` | optional | چک | 151 | +| نام بانک | `bank_name` | optional | چک | 157 | +| میزان پول | `price` | **required** | پولی | 174 | +| مظنه | `price_per_gram` | **required** | پولی | 180 | + +`views/finical/list.vue` row actions: تایید (`document.list.confirm`, line 12), رد (`document.list.deny`, 30), ویرایش (`document.list.edit`, 48), حذف (`document.list.remove`, 58), pdf (`document.list.get`, 76). +List columns (`views/finical/list_model.js`, `POST .../cash/table`): `id` (شماره سفارش, number), `type_str` (نوع سند, text), `price` (پول), `value` (وزن نهایی (گرم)), `to` (به, hidden), `customer.name` (نام و نام خانوادگی مشتری), `note` (توضیحات), `ayar` (عیار, number), `lab` (آزمایشگاه), `recive_date` (تاریخ دریافت, date), `account_name` (hidden), `bank_name` (hidden), `caj` (تاریخ ثبت, date), `uaj` (hidden), `confirmed` (وضعیت تایید سند, filterSelect `{0:'تایید نشده', 1:'تایید شده'}`, list_model.js:19-22), `price_per_gram` (مظنه, hidden), `original_value` (وزن طلای داده شده, hidden), `ang` (انگ, hidden), `opt`. + +#### `views/user/UserCreate.vue` + `views/user/ListUser.vue` — Admin CRUD + +Shared field rules (UserCreate.vue:7-60, mirrored in ListUser.vue:48-100 edit modal): + +| Label | Field | Rules | +|---|---|---| +| نام | `fname` | required «لطفا مقدار را وارد کنید»; pattern `/^([\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C ])+$/` msg «فیلد نام باید شامل حروف فارسی باشد.» | +| نام خانوادگی | `lname` | same as `fname` | +| شماره موبایل | `mobile` | required; pattern `/(\+98\|0\|98\|0098)?([ ]\|-\|[()]){0,2}9[0-9]([ ]\|-\|[()]){0,2}(?:[0-9]([ ]\|-\|[()]){0,2}){8}/` msg «فرمت شماره موبایل نادرست است.»; `min: 11` msg «شماره موبایل باید 11 رقم باشد » | +| نوع کاربری | `ut` | required «وارد کردن مقدار الزامی است»; options from `GET const/__list__ut` (UserCreate.vue:169) | +| رمز عبور | `password` | required (multi-rule block, UserCreate.vue:43-50) | +| تأیید رمز عبور | `password_confirmation` | required + match check (UserCreate.vue:55-62) | + +Calls: create `POST /users` (UserCreate.vue:108); update `PUT /users/{route.params.id}` (UserCreate.vue:104) and `PUT /users/{tempRow.id}` from the list modal (ListUser.vue:162); delete `DELETE users/{id}` (ListUser.vue:224) followed by `$refs.tbl.fetch()`. +List model `src/table/userTbl.js:2`: `GET/POST /users/list`; columns `id` (کد مدیر, noFilter), `fname` (نام, text), `lname` (نام خانوادگی, text), `mobile` (موبایل, text), `ut` (نوع کاربر, number, slot `ut`), `op` (عملیات, slot, noSort). + +#### `views/user/Permission.vue` — Role/permission editor (175 lines) + +| Field | Rule | Line | +|---|---|---| +| سطح کاربر (`ut`) | required, «لطفاسطح دسترسی را انتخاب کنید» | 9 | +| نام سطح دسترسی (`user`) | `required: newUT` (only when creating a new role) | 19 | +| نقش ها | `a-tree` over `permissionConst.js` | 23 | + +Flow: load role list `GET /const/__list__ut` (113); load a role's grants `GET /roles/{id}` (136); create a new role `POST const {key:"__list__ut", val: values.user}` (77) then `POST roles/create {ut: newUtId, roles: rid}` (80); when editing, each removed key is deleted individually with a 500 ms stagger — `DELETE /roles/{ut}/{x}` inside `setTimeout(…, 500)` (86) — then `POST roles/create {ut, roles: rid}` (93). + +#### `views/sms/create.vue` — SMS blast (265 lines) + +`type` select (required, plus `{type:'number', message:'مقدار انتخاب شده نا معتبر است'}` — create.vue:13), options each gated: + +| value | label | gate | +|---|---|---| +| `-1` | یک شماره تلفن | `sms.create.single` | +| `0` | همه کاربران | `sms.create.all` | +| `1` | کارشناسان فروش | `sms.create.reagent` | +| `2` | مشتریان عمده | `sms.create.omde` | +| `9` | چند شماره تلفن | `sms.create.multiple` | + +Other fields: `mobile` (shown when `type === -1`) required + pattern `/^(\+98|0098|98|0)?9\d{9}$/g` msg «فرمت شماره موبایل نادرست است.» (35); `mobiles` (multi) required (43); `text` (متن پیامک) required (52). +Calls: sent-list `POST /sms/table` (168); test send `POST https://henzagold.com/oapi/api/sms/batch/test` (195); real send `POST https://henzagold.com/oapi/api/sms/batch` (216, 231). Note these two hit `/oapi/api/…`, **not** the `/oapi/admin/` base. + +#### `views/notify/create.vue` — Push notification (156 lines) + +| Field | Rule | Line | +|---|---|---| +| عنوان (`title`) | required «لطفا مقدار را وارد کنید» | 7 | +| نوع کاربر (`type_send`) | required | 13 | +| کاربر (`customer_id`) | required, shown when `type_send` ∈ {3, 5} | 31 | +| متن پیام (`note`) | required «مقدار را وارد نمایید» | 55 | + +`type_send` enum (create.vue:83-90): `1: کارشناسان فروش`, `2: مشتریان عمده`, `3: یک مشتری`, `4: همه مشتریان`, `5: یک مدیر`, `6: همه مدیران`. +Calls: `POST /notify` (112); admin picker `POST /users/list` (128); customer picker `POST /notify/customer` (139). +Received list (`views/notify/user.vue` + `notifyTbl.js:3`): `POST /notify/table`, columns `id` (شماره), `title` (عنوان), `note` (متن), `opt`; `tbl.noFilters()` (notifyTbl.js:9). Mark-read `GET /notify/read/{id}`; detail `GET /notify/{id}`. + +#### `views/manage/wallet.vue` — Wallet config (142 lines) + +| Section | Field | Rule | Line | +|---|---|---|---| +| Birthday credit | `birthday_expire` (انقضا (روز)) | required | 13 | +| Birthday credit | `birthday_credit` (میزان (تومان)) | required | 18 | +| Per-order charge | `expire` (انقضا (روز)) | required | 34 | +| Per-order charge | `percent` (درصد) | required | 42 | + +Calls: `GET /wallet/config` (77), `GET /wallet/birthday/config` (91), `POST /wallet/config` (109, 126). Page gated by `config.wallet` (wallet.vue:3). + +#### `views/wallet/wallet.vue` — Charged-wallet list (103 lines) + +Page gated by `wallet.list` (line 3); percent control gated by `wallet.percent` (line 29). Model `walletTbl.js:3` → `POST /wallet/table`; columns `id` (شماره), `customer.name` (نام مشتری, text), `order.id` (شماره سفارش, text), `value` (میزان شارژ, slot `value`), `expire_at` (تاریخ انقضا, slot `time`, noSort); `tbl.noFilters()` (walletTbl.js:10). + +#### `views/statics/index.vue` — Product-view analytics (207 lines) + +`mode` select (index.vue:6-17): `title` (عنوان مسیر), `customer/all` (تمام مشتریان), `customer/id` (یک مشتری — reveals a searchable customer select). +Calls: `POST log/{mode}` or `POST log/customer/{customer}` with `{start_at, end_at}` (index.vue:146-147); `POST` fallback (171); `GET log/ago/{customer}` (174); customer list `GET /log/list/customer` (188). Columns for the `customer/*` modes nest `customer.id` (شناسه), `customer.name` (نام), `customer.mobile` (موبایل) under «مشتری», then `time` (زمان) and `title` (نام محصول) (index.vue:105-136). + +#### Customer list views (`OmdeList` / `OmdeNonVerifiedList` / `OmdeArchivedList` / `BankerList` / `ShopkeeperList`) + +Five near-identical 612–620-line views. Shared column model (differing only in URL): + +| key | title | filter | flags | +|---|---|---|---| +| `id` | کد | number | | +| `fname` | نام | text | hidden | +| `lname` | نام خانوادگی | text | hidden | +| `name` | نام و نام خانوادگی | text | | +| `mobile` | موبایل | text | hidden, noSort | +| `city_id` | شهر | select(`store.state.users.cityFilters`), `filterKey('city_id')` | hidden, slot `city` | +| `reagent.name` | کارشناس فروش | text | noSort | +| `gram_level` | سطح اعتبار | number | noSort | +| `percent` | درصد | number | noSort | +| `remain` | بدهی گرمی | number | noSort | +| `rial_remain` | بدهی ریالی | number | noSort | +| `caj` | تاریخ ثبت | date | noSort | +| `uaj` | بروزرسانی | date | hidden, noSort | +| `sts_str` | وضعیت ثبت سفارش | — | slot `sts`, noSort | +| `active` | وضعیت حساب کاربری | select | slot `active`, noSort | +| `phone` | تلفن | text | hidden, noSort | +| `address` | آدرس | text | hidden, noSort | +| `store_name` | نام مغازه | text | hidden, noSort | +| `zone` | منطقه | text | hidden, noSort | +| `pasazh_name` | نام پاساژ | text | hidden, noSort | +| `opt` | عملیات | — | slot `opt`, noSort | + +(OmdeListModel.js:8-42 and the four siblings, identical line numbering.) + +| View | Table URL | +|---|---| +| OmdeList | `POST .../customer` (OmdeListModel.js:7) | +| OmdeNonVerifiedList | `POST .../major/verify/table` (OmdeListNonVerifiedModel.js:7) | +| OmdeArchivedList | `POST .../major/archive/table` (OmdeArchiveListModel.js:7) | +| BankerList | `POST .../banker` (BankerListModel.js:7) | +| ShopkeeperList | `POST .../shopkeeper` (shopkeeperListModel.js:7) | + +Row actions (all five, gates listed in §5.3): ویرایش, مشاهده, مشاهده سفارشات, مشاهده اسناد مالی, توقف سفارش / فعال سازی سفارش, فعال سازی / غیر فعال سازی حساب. Endpoints used: `POST customer/toggle-active`, `POST customer/toggle-sts`, `POST major/verify`, `GET major/archive/{id}`, `GET major/dashboard/{id}`. + +`ReagentList.vue` uses `POST .../reagent` (ReagentListModel.js:7) with `grams_sell` (میزان فروش) added and the shop/zone columns removed (ReagentListModel.js:21). + +`SingleCustomer.vue` (`SingleCustomerList.js:3` → `POST .../single-customer`): `id` (کد, number), `fname` (نام, text), `lname` (نام خانوادگی, text, noSort), `phone` (تلفن تماس, text, noSort), `email` (ایمیل, text, noSort), `branch.name` (نام شعبه, text, noSort), `op` (عملیات). Wallet-charge actions gated by `wallet.charge` (SingleCustomer.vue:18, 26, 37). + +#### Detail pages (`ViewOmdeCustomer.vue`, `ViewSingleCustomer.vue`) + +Tabbed 360° views built from four XTbl models each: + +| Tab | Wholesale model → URL | Retail model → URL | +|---|---|---| +| Customers | `omdeCustomer.js:4` → `.../major/customer` | — | +| Orders | `omdeOrder.js:4` → `.../major/order` | `singleOrder.js:4` → `.../single/order` | +| SMS | `omdeSms.js:3` → `.../major/sms` | `singleSms.js:3` → `.../single/sms` | +| Transactions | `omdeTransaction.js:3` → `.../major/cash` | `singleTransaction.js:3` → `.../single/transaction` | + +Transaction columns (`omdeTransaction.js:4-7`): `id` (شماره), `order.id` (شماره سفارش, text, `setDefaultValue('-')`), `value` (میزان شارژ, slot), `expire_at` (تاریخ انقضا, noSort, `setDefaultFilter('jdate')`, `setDefaultValue('-')`). +SMS columns (`omdeSms.js:4-8`): `id`, `massage` (متن پیامک, text), `mobile` (شماره تلفن, slot `value`), `created_at` (تاریخ انقضا, `setDefaultFilter('jdate')`, `setDefaultValue('-')`), `sts` (وضعیت, slot). +Other calls: `GET single/{id}`, `GET single/order/{id}`, `GET single/report/{id}`, `GET single/wallet/{id}`, `GET log/ago/{id}`. + +#### `views/report/*` + +`order_item.vue` model (`order_item_model.js:3` → `POST .../report/order_item`) columns: `id` (کد آیتم), `product_title` (نام محصول), `variant_title` (عنوان تنوع), `original_rash_weight` (وزن خام), `rash_weight` (وزن نهایی), `qty` (تعداد), `percent` (درصد), `rial_pay` (اجرت ریالی), `note` (توضیحات), `order_id` (شماره سفارش), `order.caj` (تاریخ ثبت شفارش), `order.cashe.id` (شماره سند), `order.cashe.type_str` (نوع سند), `order.cashe.value` (جمع کل سند), `order.customer.fname`, `order.customer.lname`, `order.customer.name`, `order.parent.name` (کارشناس فروش) — order_item_model.js:4-21. + +`product.vue` model (`src/table/variant.js:3` → `POST /product/report`) columns: `id` (کد تنوع), `product_id_id` (کد محصول, hidden), `product.title` (نام محصول, `filterKey('product')`), `title` (نام تنوع), `qty` (تعداد), `grams` (وزن), `color_str` (رنگ, slot `color`), `rial_pay` (اجرت ریالی), `percentage_pay` (اجرت درصدی), `op` (عملیات). Edit action → `window.open('/admin/apps/product/edit/{product.id}/{row.id}','_blank')` (product.vue:101), gate `product.list.edit` (line 14). + +#### Other list models + +| View | Model file:line | URL | Columns | +|---|---|---|---| +| Branch list | `views/branch/ListBranch.js:3` | `POST .../branch/table` | `name` (نام, text), `address` (آدرس, text), `phone` (شماره تلفن, number), `user.name` (مدیر شعبه, number), `op` (slot `actions`) | +| Branch buy list | `views/branch/orderListModel.js:4` | `POST .../branch/product/table` | `id` (شماره, number), `count_item` (تعداد آیتم ها), `sum_weight` (جمع وزن), `caj` (تاریخ ثبت, date), `uaj` (hidden), `sts_str` (وضعیت, conditional), `note` (توضیحات), `opt` | +| Workshop list | `views/workshop/ListWorkshop.js:3` | `POST .../workshop/table` | `name`, `address`, `phone` (number), `op` (slot `actions`); `_noDownload = true` (line 10) | +| Workshop buy list | `views/workshop/orderListModel.js:4` | `POST .../workshop/product/table` | `id`, `count_item`, `sum_weight`, `caj`, `uaj` (hidden), `opt` | +| Invoice list | `views/invoice/ListInvoice.js:4` | `POST .../branch/product/table` | `id` (ردیف, number), `name` (نام مشتری, text), `sum_weight` (جمع کل سفارش, number, noSort), `count_item` (تعداد آیتم ها, number, noSort), `caj` (تاریخ ثبت, date, noSort), `allocator_user.name` (ثبت کننده, slot `user`, noSort), `note` (توضیحات, text, noSort), `op` (slot `actions`) | +| Invoice product picker | `views/invoice/AddInvoice.js:3` | `POST .../products` | `id`, `ProductImage` (slot `image`), `title`, `min_gram` (وزن, number), `brand.title`, `category.collection.title`, `product_code`, `max_gram` (حداکثر وزن, hidden), `op`; `_noDownload = true` (line 16) | +| Order product picker | `views/order/product.js:3` | `POST .../products` | `id`, `ProductImage`, `title`, `brand.title`, `category.collection.title`, `product_code`, `min_gram` (hidden), `max_gram` (hidden), `op`; `_noDownload = true` | +| Category list | `views/apps/category/CategoryTbl.js:3` | `POST .../category` | `id` (شماره, number), `image` (تصویر, slot, noSort), `type_str` (نوع نمایش), `title` (عنوان دسته), `parent.title` (پدر دسته), …, `opt` | +| Promotion list | `src/table/list_model.js:3` | `POST .../major/promotion/table` | `id` (شماره تخفیف, number), `title` (عنوان), `description` (توضیحات), `type_str` (نوع تخفیف), `start_atj` (تاریخ شروع), `end_atj` (تاریخ پایان), `type_customer` (نوع مشتری, slot), `opt` | +| Comments | `views/customer/Comments/CommentListModel.js:4` | `POST .../comment/table` | `product_id` (hidden, number), `product.title` (نام محصول, slot `productName`), `customer.id` (hidden), `customer` (نام نویسنده, slot), `reply` (پاسخ, slot, noSort), `sts` (وضعیت, slot, noSort), `changeState` (conditional), `opt` | +| City list | `views/city/CityListModel.js:3` | `POST .../cities/list` | (model constructed with no `.add()` calls — columns built in the view) | +| Brand list | `src/table/brand.js:3` | `POST .../brand` | `id` (کد, number), `title` (نام برند, text) | +| Newsletter | `views/newsletters/newsletter.js:3` | `POST .../fashion/table` | `name` (نام, text), `mobile` (شماره موبایل, number), `op` (slot `actions`); `_noDownload = true` | +| Wholesale customer (alt) | `src/table/major_customer.js:3` | `POST .../major/table` | `id`, `lname` (hidden), `name` ×2, `mobile` (hidden), `gram_level`, `percent`, `remain`, `rial_remain`, `grams_sell`, `caj`, `uaj` (hidden) | + +--- + +## 7. Store + +`src/store/store.js` — root with `state/getters/mutations/actions` + 11 namespaced modules (store.js:39-60). `strict: process.env.NODE_ENV !== 'production'` (store.js:59). + +| Module key | File | Purpose | +|---|---|---| +| `cart` | `store/cart/cart.js` | Retail order cart | +| `invoiceCart` | `store/invoiceCart/invoiceCart.js` | Transfer-invoice cart (near-identical copy of `cart`) | +| `auth` | `store/auth/moduleAuth.js` | **Live** auth (login, token/user persistence) | +| `category` | `store/category/index.js` | Category CRUD | +| `variant` | `store/variant/index.js` | Product variants | +| `product` | `store/product/index.js` | Product CRUD | +| `promotion` | `store/promotion/index.js` | Variant promotions | +| `order` | `store/order/index.js` | Order CRUD | +| `customer` | `store/customer/index.js` | Customer CRUD | +| `eCommerce` | `store/eCommerce/moduleECommerce.js` | Vuexy template demo | +| `variantpromotion` | `store/variantpromotion/index.js` | Per-variant promotion rows | +| `users` | `store/users/index.js` | Roles/permissions + city filter cache | + +**Registered but commented out** (store.js:21-24): `todo`, `calendar`, `chat`, `email`. `manage/const.js` is imported as `moduleConst` but **not registered** (store.js:29, 48 — line 48 is commented). +Directories present and unused: `store/data-list/`, `store/user-management/`, `store/manage/index.js` (empty file). + +### Root state — `src/store/state.js:73-91` + +`AppActiveUser` (merged `localStorage.userInfo` over defaults, state.js:18-46), `bodyOverlay`, `isVerticalNavMenuActive`, `is_touch_device`, `mainLayoutType` (from themeConfig), `navbarSearchAndPinList`, `reduceButton`, `verticalNavMenuWidth`, `verticalNavMenuItemsMin`, `scrollY`, `starredPages`, `theme`, `themePrimaryColor`, `windowWidth`, `orderCount` (state.js:90). +Root mutations of note: `setOrderCount` (mutations.js:20-22), `UPDATE_USER_INFO` (mutations.js:100-121, also writes `localStorage.userInfo`), layout/theme toggles (mutations.js:23-92). + +### `auth` — `src/store/auth/moduleAuth.js` + +| Item | Detail | +|---|---| +| `state.user` | function reading `localStorage[isBranch()?'branch_user':'user']` (moduleAuth.js:5-7) — **a function, not a value**; `router.js:164` truth-tests it, so it is always truthy | +| `state.token` | function reading `localStorage[isBranch()?'branch_token':'token']` (moduleAuth.js:8-10) — same caveat | +| `state.userPermission` | `[]` (moduleAuth.js:11) — unused | +| action `login(payload)` | `POST admin-login` → `dispatch('users/getRoles', data.roles.map(x=>x.rid), {root:true})`, then `delete data.roles; delete data.ut`, `commit('INJECT', data)`; rethrows on error (moduleAuth.js:14-24) | +| mutation `INJECT` | writes `localStorage[token|branch_token] = payload.api_token`, `localStorage.login = true`, `localStorage[user|branch_user] = JSON.stringify(payload)` (moduleAuth.js:28-32) | + +Dead siblings: `moduleAuthActions.js` (firebase + jwt flows, 362 lines), `moduleAuthState.js` (firebase `isUserLoggedIn`), `moduleAuthMutations.js` (`SET_BEARER`), `moduleAuthGetters.js` (empty). + +### `users` — `src/store/users/index.js` + +State: `cityFilters: {}`, `userInfo: undefined`, `roles: undefined` (index.js:23-27). + +| Action | Endpoint / behavior | +|---|---| +| `getCities` | `POST cities/list` → `commit('setCities', res.data.rows)` which maps `{[x.id]: x.title}` (index.js:51-57, 30-34) | +| `setUserInfo` / `deleteUserInfo` | local only (index.js:58-63) | +| `getRoles(roles)` | `commit('getRoles', checkParent(roles))` — expands every dotted key to all ancestor prefixes (index.js:64-66, 3-21) | +| `deleteRoles` | clears `state.roles` (index.js:67-69) | + +### CRUD modules — endpoints per action + +| Module | Action | HTTP | Endpoint | Line | +|---|---|---|---|---| +| `order` | `remove(id)` | DELETE | `order/{id}` then `dispatch('order/fetch')` | order/index.js:42-43 | +| `order` | `save(payload)` | PUT / POST | `order/{id}` if `payload.id` else `order/` | order/index.js:54, 57 | +| `order` | `fetch` | GET | `order/` params `{page: state.page, text_search: state.searchQuery}` | order/index.js:69 | +| `product` | `init` | GET | `config/statics` | product/index.js:46 | +| `product` | `remove(id)` | POST | `delete/product/{id}` then `fetch` | product/index.js:58-59 | +| `product` | `save` | POST | `product` | product/index.js:78 | +| `product` | *(2nd save)* | PUT / POST | `product/{id}` else `product` | product/index.js:91, 94 | +| `product` | `fetch` | GET | `product/` params `{page, text_search}` | product/index.js:106 | +| `product` | *(variant)* | PUT | `product/variant/{id}` | product/index.js:115 | +| `customer` | `remove(id)` | DELETE | `user/profile/{id}` then `fetch` | customer/index.js:44-45 | +| `customer` | `save` | PUT / POST | `user/profile/{id}` else `user/profile/` | customer/index.js:56, 59 | +| `customer` | `omdeSave` | POST | `customer/create` | customer/index.js:71 | +| `customer` | `fetch` | GET | `user/profile` params `{page, text_search}` | customer/index.js:82 | +| `customer` | `getEditData({id})` | GET | `user/profile/{id}` | customer/index.js:94 | +| `category` | `remove(id)` | DELETE | `category/{id}` then `fetch` | category/index.js:39-40 | +| `category` | `save` | PUT / POST | `category/{id}` else `category/` | category/index.js:52, 55 | +| `category` | `fetch` | GET | `category/` params `{page, text_search}` | category/index.js:67 | +| `variant` | `init` | GET | `config/statics` | variant/index.js:38 | +| `variant` | `save` | POST | `add/variant` (both create and "update" paths) | variant/index.js:51, 54 | +| `variant` | `remove(row)` | DELETE | `/product/variant/{row.id}` | variant/index.js:66 | +| `promotion` | `init` | GET | `config/statics` | promotion/index.js:46 | +| `promotion` | `remove(id)` | DELETE | `product/variant/promotion/{id}` then `fetch` | promotion/index.js:58-60 | +| `promotion` | `save` | PUT / POST | `product/variant/promotion/{id}` else `product/variant/promotion` | promotion/index.js:70, 73 | +| `promotion` | `fetch` | GET | `product/variant/promotion` params `{page, text_search}` | promotion/index.js:85 | +| `variantpromotion` | `remove(id)` | DELETE | `product/variant/promotion/variant-promotion/{id}` | variantpromotion/index.js:58 | +| `variantpromotion` | `save` | PUT / POST | `product/variant/promotion/variant-promotion/{id}/` else `product/variant/promotion/variant-promotion` | variantpromotion/index.js:71, 75 | +| `variantpromotion` | `fetch(id)` | GET | `/product/variant/promotion/{id}/variant-promotion` | variantpromotion/index.js:88 | +| `cart` / `invoiceCart` | `init` | POST | `product/variant` body `{products_id: ids}` | cart/cart.js:73; invoiceCart.js:68 | + +Shared list-module state shape (`order`, `customer`, `category`, `product`, `promotion`, `variantpromotion`): `{model, rows, count, page, loading, searchQuery}` with mutations `ADD_ITEM`, `LOADING`, `SET_PAGE`/`PAGE_SET`, `SEARCH_QUERY_SET`, `FETCH`, `INJECT_LIST`, `EDIT_ROW` (e.g. order/index.js:3-36). + +### `cart` / `invoiceCart` business rules + +| Rule | Evidence | +|---|---| +| Identity key is the pair `(pid, vid)` | cart/cart.js:11 | +| An item is only addable if `variant && !(!variant.is_pre_order && variant.qty === 0)` — i.e. out-of-stock is allowed only when the variant is pre-order | cart/cart.js:15; invoiceCart.js:15 | +| Re-hydration (`init`) re-applies the same stock rule against `variants_all[0]` and drops items that fail | cart/cart.js:79; invoiceCart.js:74 | +| Each rebuilt item carries `{vid, pid, product, qty, promotion_id: product.promotion?.id ?? null}` | cart/cart.js:80-86 | +| `INIT` is committed after every mutation (persistence hook) | cart/cart.js:52-68 | + +--- + +## 8. Shared utils / filters / mixins + +### `src/utils/index.js` + +| Export | Behavior | Line | +|---|---|---| +| `isBranch()` | `process.env.NODE_ENV == 'branch'` (also `console.log`s the env) | utils/index.js:1-4 | + +This single flag drives: token/user localStorage key (axios.js:6, moduleAuth.js:6/9, router.js:173/174/188), route table (router.js:27, 39), menu (navMenuItems.js:609), layout type (themeConfig.js:23), page titles (order/List.vue:6). + +### `src/filters/filters.js` — Vue global filters + +| Filter | Signature | Behavior | Line | +|---|---|---|---| +| `capitalize` | `(value)` | Title-cases each space-separated word | 4-14 | +| `title` | `(value, replacer='_')` | Splits on `replacer`, capitalizes, re-joins with space | 16-27 | +| `truncate` | `(value, limit)` | `value.substring(0, limit)` | 29-31 | +| `tailing` | `(value, tail)` | `value + tail` | 33-35 | +| `time` | `(value, is24HrFormat=false)` | `H:MM` (24h) or `h:MM بعد از ظهر / قبل از ظهر` (12h) | 41-53 | +| `date` | `(value, fullDate=false)` | Slices a JS `Date` string: `DD MMM` or `DD MMM YYYY` | 55-63 | +| `month` | `(val, showYear=true)` | Regex-extracts month (+ year) from a Date string | 65-75 | +| `csv` | `(value)` | `value.join(', ')` | 77-79 | +| `filter_tags` | `(value)` | Strips HTML tags via `/<\/?[^>]+(>|$)/g` | 81-83 | +| `k_formatter` | `(num)` | `num > 999 ? (num/1000).toFixed(1)+'هزار' : num` | 85-87 | +| **`price`** | `(val)` | Thousand separators: `val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")`; returns `val` unchanged when falsy (so `0` passes through) | 92-97 | +| **`priceUnit`** | `(val)` | Same as `price` + `' تومان '` suffix | 99-104 | +| **`jdateTime`** | `(val)` | `moment(val).format('jYYYY/jMM/jDD - hh:mm:ss')` — note **12-hour `hh` with no am/pm marker** | 107-112 | +| **`jdate`** | `(val)` | `moment(val).format('jYYYY/jMM/jDD')` | 114-119 | +| `dynamicFilter` | `(value, name, ...args)` | `Vue.filter(name)(value, ...args)` — used by XTbl `setDefaultFilter` | 120-122 | +| `none` | `val => val` | Identity — the default for `setDefaultFilter` | 123 | + +**Notable absences**: there is **no gram/weight formatter**, **no Persian↔English digit-conversion utility**, and **no price-input mask** anywhere in the codebase. Weights are rendered raw (`{{item.grams}}`, order/List.vue:173) and entered through plain `a-input-number`. Prices are formatted only on read via `price`/`priceUnit`. + +### `src/mixins/citiesModel.js` + +| Method | Behavior | Line | +|---|---|---| +| `normalizeCityId(data)` | Clones `data`; if `city_id` is null and `city` is set, resolves it — from `city.id` when `city` is an object, or by title match against `this.cities` when it's a string; finally coerces `city_id` to `Number` | mixins/citiesModel.js:3-18 | +| `recursiveFunction(obj, level)` | Builds an ant `a-cascader`/`a-tree-select` node tree from the flat `this.cities` array using `level`/`parent`; `disabled: level === 0` (provinces not selectable); emits `{value:Number(id), label:title, key:Number(id), disabled, children?}` | mixins/citiesModel.js:19-46 | + +`src/views/apps/product/uploadMixin.js` — product image upload helpers. +`src/components/input/mixins.js` — shared form-input mixin (see §9). + +### Constants / enums (exact values) + +| Domain | Values | Source | +|---|---|---| +| Wholesale order status (filter) | `0: معلق`, `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی `, `5: تحویل مسئول فروش`, `6: تحویل مشتری ` | views/order/OmdeListModel.js:23-29 | +| Retail order status (create form) | `1: ثبت سفارش`, `2: تایید مدیر سیستم`, `3: در حال تامین`, `4: در حال آماده سازی`, `5: ارسال`, `6: تحویل مشتری` | views/order/create.vue:158-165 | +| Customer confirmation | `1: تایید شده`, `2: در انتظار تایید` | views/order/OmdeListModel.js:35-36; TaminListModel.js:19-20 | +| Financial document type | `1: آب شده`, `2: حواله`, `3: چک`, `4: پولی` | views/finical/addFinDoc.vue:19-31 | +| Document confirmation | `0: تایید نشده`, `1: تایید شده` | views/finical/list_model.js:20-21 | +| Product display type | `1: سایت`, `2: اپلیکشن موبایل`, `3: سایت و اپلیکشن موبایل` (default `3`) | views/apps/product/AddProduct.vue:23-33 | +| Delivery type | `1: کوتاه مدت`, `2: بلند مدت` (default `1`) | views/apps/product/AddProduct.vue:132-141 | +| SMS audience | `-1: یک شماره تلفن`, `0: همه کاربران`, `1: کارشناسان فروش`, `2: مشتریان عمده`, `9: چند شماره تلفن` | views/sms/create.vue:15-29 | +| Notification audience | `1: کارشناسان فروش`, `2: مشتریان عمده`, `3: یک مشتری`, `4: همه مشتریان`, `5: یک مدیر`, `6: همه مدیران` | views/notify/create.vue:83-90 | +| Statics mode | `title` (عنوان مسیر), `customer/all` (تمام مشتریان), `customer/id` (یک مشتری) | views/statics/index.vue:7-16 | +| Default transfer target | `'حواله کوثر'` + server `havale_list` | views/finical/addFinDoc.vue:121-127 | +| Const-manager keys | `{id:'brand', title:'برند ها', icon:'icon-user', iconPack:'feather'}` (`ut`/`status` entries commented out) | store/manage/const.js:6-25 | +| Theme colors | `primary:#1c2d56`, `secondary:#836447`, `third:#f5cb55`, `success:#28C76F`, `danger:#EA5455`, `warning:#FF9F43`, `dark:#1E1E1E` | themeConfig.js:5-14 | + +--- + +## 9. Components + +### 9.1 Table system — `src/components/x-table/` + +The workhorse. Four near-duplicate copies exist: `x-table/` (**canonical**), `x-table_select/`, `custom-xtable-select/`, `backup-table/`. Only `x-table` is imported by business views. + +**`model/XTbl.js`** — fluent table builder: + +| Member | Purpose | Line | +|---|---|---| +| `constructor(url, title)` | sets the POST endpoint and the report title | XTbl.js:10-13 | +| `add(col)` | appends an `Xtc` | XTbl.js:15-20 | +| `addDefaultFilter(filter)` | pre-applied filter | XTbl.js:22-28 | +| `addOpt(...)` | appends an «عملیات» column with `noSort()` | XTbl.js:31-38 | +| `hidePagination()` / `hideSearch()` / `addExtraBtn()` | all set `_hidePageination = true` (the latter two are copy-paste bugs) | XTbl.js:41-59 | +| `getSortFields()` | cols where `__sorter` is truthy | XTbl.js:47-49 | +| `noSearch()` / `noSettings()` / `noFilters()` | set `_noSearch` / `_noSettings` / `_noFilters` | XTbl.js:61-74 | + +**`model/Xtc.js`** — column builder. Defaults: `dataIndex = key`, `show = true`, `__sorter = true` (Xtc.js:4-12). + +| Method | Effect | Line | +|---|---|---| +| `filterKey(key)` | overrides the server-side filter field (`__fkey`/`master`) | Xtc.js:33-37 | +| `renderSlot(name)` | sets `scopedSlots.customRender` | Xtc.js:39-48 | +| `filterText()` | `filterType = 'default'` | Xtc.js:59-63 | +| `filterSwitch()` | `filterType = 'switch'` | Xtc.js:65-70 | +| `filterQuery(action)` | `filterType = 'query'` + `filterAction` | Xtc.js:73-78 | +| `filterDate()` | `filterType = 'date'` | Xtc.js:80-84 | +| `filterSelect(items)` | `filterType = 'select'` + `filterItems` | Xtc.js:86-91 | +| `filterNumber()` | `filterType = 'number'` | Xtc.js:93-97 | +| `noSort()` | `__sorter = false` | Xtc.js:99-102 | +| `hidden()` | `show = false` (still toggleable in the column selector) | Xtc.js:104-107 | +| `setDefaultValue(v)` / `setDefaultFilter(v)` | delegate to `sedmedghAddon/XtcModel` mixins — `setDefaultFilter` names a Vue filter applied via `dynamicFilter` | Xtc.js:109-115 | + +**`XTable.vue`** — the rendering component (ant `a-table` based): + +| Aspect | Detail | Line | +|---|---|---| +| Query shape | `{search, filters: [], limit: 10, page: 1, order}` | XTable.vue:316-320 | +| Default page size | `10`; selectable `['5','10','20']` | XTable.vue:255-256, 319 | +| Sort encoding | `+field` (asc) / `-field` (desc) from the `sort` + `sortDirection` controls | XTable.vue:340-343 | +| Header-click sort | `this.query.order = ['{-|+}{columnKey}']` — **inverted vs. the dropdown**: `ascend` maps to `-` | XTable.vue:445-451 | +| Request body | `{...query, select: visibleCols.map(c=>c.key), titles: {key: title}, report_title: model.title}` (+ `op` when exporting) | XTable.vue:466-491 | +| Filter pruning | drops filters with no `value` unless `op ∈ {'null','nn'}` or `value == 0` | XTable.vue:469 | +| Page reset | if the filter+search hash changed since the last request → `query.page = 1` | XTable.vue:472-481 | +| Fetch | `await axios.post(this.model.url, this.requestData(op))`; on `op ∈ {'excel','json','print'}` → `window.open(data.url)` and return | XTable.vue:494-512 | +| Export buttons | json (line 96), excel (107), print (118) | XTable.vue:96-118 | +| Search | `a-input-search` → sets `query.search`, refetches | XTable.vue:10-15, 519-523 | +| Filter UI | dynamic rows via `XFilterRow`; add row pushes `{id: rand}` (XTable.vue:410-412); clear all disabled when empty (171) | XTable.vue:131-175 | +| Column selector | `visibleCols = cols.filter(c => c.show)` | XTable.vue:377-381 | +| Fullscreen | `openFullscreen`/`closeFullscreen` from `x-table/fullscreen.js` | XTable.vue:456-465 | +| Lifecycle | `mounted()` → `fetch()` | XTable.vue:384 | + +Filter widgets (`x-table/components/`): `TextXTableFilter.vue`, `NumberXTableFilter.vue`, `DateXTableFilter.vue` (Jalali), `SelectOptionXTableFilter.vue`, `SwitchXTableFilter.vue`, `QuerySelectTableFilter.vue`, `XFilterRow.vue`. + +### 9.2 Form inputs — `src/components/input/` + +**`mixins.js`** — shared by all input components: + +| Prop | Purpose | +|---|---| +| `field` | the `v-decorator` key | +| `items`, `form`, `formClass`, `label`, `help`, `show_help` | presentation | +| `initialValue` | merged into `_options.initialValue` (mixins.js:43-45) | +| `required` | when truthy, **auto-generates** `rules: [{required: true, message: \`وارد کردن ${label} الزامی است\`}]` (mixins.js:46-53) | +| `options` | raw ant decorator options, merged first (mixins.js:39-42) | + +Computed `status` proxies `$refs.fi.getValidateStatus()` (mixins.js:29-34). + +**`DateInput.vue`** (`name: 'JalaliInput'`) — Jalali date picker: + +| Aspect | Detail | Line | +|---|---|---| +| Widget | `vue-persian-datetime-picker`, `color="#1c2d56"`, `clearable`, `auto-submit` | DateInput.vue:9-15 | +| Bound value | hidden `` — the picker writes into the ant form imperatively | DateInput.vue:7 | +| Default format | `jYYYY/jMM/jDD` when no `format` prop | DateInput.vue:49 | +| Props | `format`, `displayFormat` | DateInput.vue:29-32 | +| Clear | `null` → sets the field to `null` via `$refs.fi.FormContext.form.setFieldsValue` | DateInput.vue:44-47 | +| Sync | `updated()` re-reads the field value back into the picker | DateInput.vue:57-59 | + +**Uploaders**: + +| Component | Upload target | Notes | +|---|---|---| +| `input/UploadInput.vue` | `baseURL + 'file/create'` (UploadInput.vue:72) | multi-file; a second definition in the same file uses `themeConfig.home('cu/file/create')` (line 215) — dead branch | +| `input/SingleUploadInput.vue` | `` `${baseURL}file/create` `` (SingleUploadInput.vue:52) | single file + preview modal | +| `input/SingleUploadInputPropList.vue` | same pattern | prop-driven list variant | +| `input/PodcastUploadInput.vue` | same pattern | audio | +| `x-upload/XUpload.vue` | `props.url`; `axios.post(url, formData, {'Content-Type':'multipart/form-data'})` | XUpload.vue:41-42, 69 | +| `components/MyUpload.vue`, `views/components/MyUpload.vue` | image cropper wrapper | | +| `excel/ImportExcel.vue` | client-side xlsx import | | + +**`query-selector/QuerySelector.vue`** — debounced remote-search select: + +| Aspect | Detail | Line | +|---|---|---| +| Props | `vDecorator`, `url` | QuerySelector.vue:42-45 | +| Debounce | 300 ms `setTimeout` (cancelled on each keystroke) | QuerySelector.vue:27-38 | +| Request | `axios.get(url, {params: {text_search: value}})` | QuerySelector.vue:34 | +| Option shape | `{{ d.title }}` | QuerySelector.vue:15-19 | +| Config | `show-search`, `:filter-option="false"` (server-side filtering), `:not-found-content="null"`, `:show-arrow="false"`, placeholder `جستجو` | QuerySelector.vue:2-14 | + +### 9.3 Other components + +| Component | Purpose | +|---|---| +| `tooltip-btn/TooltipBtn.vue` | Icon button + tooltip + optional `pop-title` confirm — the standard row-action control | +| `ZoomableImage.vue` | Click-to-zoom thumbnail (used in order/product detail modals) | +| `vx-card/VxCard.vue` | Page card wrapper — globally registered (globalComponents.js:3, 10) | +| `vx-list/VxList.vue`, `vx-input-group/VxInputGroup.vue`, `FeatherIcon.vue` | globally registered (globalComponents.js:11-14) | +| `vx-auto-suggest/VxAutoSuggest.vue` | Autocomplete (template) | +| `vx-table/VxTable.vue` | Legacy table (template) | +| `goldPrice.vue` | Live gold-price widget | +| `Moshtarian.vue` | Customer picker widget | +| `statistics-cards/` + `chartConfigs.js` | Dashboard stat cards | +| `timeline/VxTimeline.vue` | Timeline (template) | +| `icon/index.vue` + `icons.json` | Icon browser | +| `ChangeTimeDurationDropdown.vue` | Dashboard date-range picker | +| `VxTour.vue` | Product tour (disabled — `themeConfig.disableThemeTour = true`, themeConfig.js:20) | +| `vue-select` | registered globally with custom Deselect/OpenIndicator (globalComponents.js:18-40) | + +--- + +## 10. Env / Config + +### `auth_config.json` + +```json +{ "domain": "YOUR_DOMAIN", "clientId": "YOUR_CLIENT_ID" } +``` +Unfilled Auth0 placeholders. `src/plugins/auth.js` and `src/auth/authService.js` reference it, but `AuthPlugin` is **commented out** at `src/main.js:36-38`. **Dead config.** + +### `themeConfig.js` + +| Key | Value | Line | +|---|---|---| +| `colors.primary` | `#1c2d56` | themeConfig.js:6 | +| `colors.secondary` | `#836447` | themeConfig.js:7 | +| `colors.third` | `#f5cb55` | themeConfig.js:8 | +| `colors.success` / `danger` / `warning` / `dark` | `#28C76F` / `#EA5455` / `#FF9F43` / `#1E1E1E` | themeConfig.js:10-13 | +| `disableCustomizer` | `false` | themeConfig.js:19 | +| `disableThemeTour` | `true` | themeConfig.js:20 | +| `footerType` | `'hidden'` | themeConfig.js:21 | +| `hideScrollToTop` | `false` | themeConfig.js:22 | +| `mainLayoutType` | `isBranch() ? 'horizontal' : 'vertical'` | themeConfig.js:23 | +| `navbarColor` | `'#fff'` | themeConfig.js:24 | +| `navbarType` | `'floating'` | themeConfig.js:25 | +| `routerTransition` | `'zoom-fade'` | themeConfig.js:26 | +| **`rtl`** | `true` | themeConfig.js:27 | +| `sidebarCollapsed` | `false` | themeConfig.js:28 | +| `theme` | `'light'` | themeConfig.js:29 | +| `userInfoLocalStorageKey` | `'userInfo'` | themeConfig.js:31 | + +Side effect: `Vue.use(Vuesax, {theme:{colors}, rtl: themeConfig.rtl})` at themeConfig.js:34. + +### Environment variables + +| File | Content | +|---|---| +| `.env.branch` | `NODE_ENV="branch"` (the only env file in the repo) | + +`process.env` reads across the codebase: + +| Reference | Location | +|---|---| +| `process.env.NODE_ENV == 'branch'` | src/utils/index.js:3 (the `isBranch()` switch) | +| `process.env.BASE_URL` | src/router.js:16 (router base) | +| `process.env.NODE_ENV` | src/store/store.js:59 (`strict` mode) | +| `process.env.NODE_ENV == 'branch'` | vue.config.js:3-4 (publicPath / outputDir) | + +**No `VUE_APP_*` variables exist.** The API base URL is hard-coded at `src/axios.js:5`, and several table models and views re-hard-code the absolute `https://henzagold.com/oapi/admin/...` prefix rather than relying on `baseURL`. + +### Other config files + +| File | Notes | +|---|---| +| `babel.config.js` | preset `@vue/app` | +| `postcss.config.js` | tailwind + autoprefixer + postcss-rtl | +| `tailwind.config.js` (28 600 B) / `tailwind-v0.js` (26 430 B) | Tailwind theme; the `-v0` file is an unused backup | +| `.browserslistrc`, `.editorconfig`, `.eslintrc.js`, `.eslintignore` | tooling | +| `src/firebase/firebaseConfig.js` | imported only from a commented line (main.js:32-33) — dead | +| `src/i18n/i18n.js`, `i18nData.js` | vue-i18n set up (main.js:64-65); menu labels use a literal `i18n` field of Persian text rather than translation keys | +| `src/fake-db/*` | axios-mock-adapter fixtures; disabled (main.js:25-26) | + +--- + +## Appendix A — Complete API endpoint index (as called from views / tables / components) + +Relative paths resolve against `https://henzagold.com/oapi/admin/` (src/axios.js:5). + +### Auth / user / roles +| Method | Endpoint | Caller | +|---|---|---| +| POST | `admin-login` | store/auth/moduleAuth.js:16 | +| GET | `users/show` | src/router.js:170 | +| POST | `/users` | views/user/UserCreate.vue:108 | +| POST | `/users/list` | views/notify/create.vue:128; src/table/userTbl.js:2 | +| PUT | `/users/{id}` | views/user/UserCreate.vue:104; views/user/ListUser.vue:162 | +| DELETE | `users/{id}` | views/user/ListUser.vue:224 | +| GET | `/user/list` | (listed in grep index) | +| GET | `/roles/{id}` | views/user/Permission.vue:136; VerticalNavMenu.vue:191 | +| POST | `roles/create` | views/user/Permission.vue:80, 93 | +| DELETE | `/roles/{ut}/{key}` | views/user/Permission.vue:86 | + +### Products / variants / categories / brands +| Method | Endpoint | Caller | +|---|---|---| +| POST | `.../products` | table/product.js:20; views/order/product.js:3; views/invoice/AddInvoice.js:3; table/promotion_product.js:20 | +| POST | `product/create` | views/apps/product/AddProduct.vue:660 | +| PUT | `product/update/{id}` | AddProduct.vue:698; order/create.vue:341 | +| GET | `product/show/{id}` | AddProduct.vue:718; order/create.vue:360 | +| POST | `delete/product/{id}` | ProductList.vue:670; store/product/index.js:58 | +| POST | `get/variants` | ProductList.vue:631 | +| GET | `get/variant` | (grep index) | +| GET | `product/{id}/variant` | ProductList.vue:665 | +| POST | `add/variant` | order/create.vue:304; store/variant/index.js:51,54 | +| PUT | `product/variant/{id}` | store/product/index.js:115; store/promotion/index.js:94 | +| DELETE | `/product/variant/{id}` | store/variant/index.js:66 | +| POST | `product/variant` | store/cart/cart.js:73; store/invoiceCart/invoiceCart.js:68 | +| GET | `/product/color` | AddProduct.vue:857 | +| GET | `product/warranty/{id}` | ProductList.vue:556 | +| GET | `product/exist/{id}` | ProductList.vue:558 | +| GET | `/product/offer/{id}` | ProductList.vue:570 | +| POST | `/change/all` | ProductList.vue:582 | +| POST | `/change/percent/count` | views/apps/product/ChangePercent.vue | +| POST | `/product/report` | table/variant.js:3 | +| POST/GET | `category` | AddProduct.vue:976; CategoryTbl.js:3; store/category/* | +| DELETE | `category/{id}` | store/category/index.js:39 | +| PUT | `category/{id}` | store/category/index.js:52 | +| GET | `brand` | AddProduct.vue:986; table/brand.js:3 | +| POST | `/brand/create` | views/manage/Const.vue | +| PUT | `/brand/` | views/manage/Const.vue | +| DELETE | `/brand/` | views/manage/Const.vue | +| GET | `.../ciny` | AddProduct.vue:991 | +| POST/PUT/DELETE | `/ciny`, `/ciny/` | views/manage/Const.vue | + +### Orders +| Method | Endpoint | Caller | +|---|---|---| +| POST | `.../single-order` | views/order/orderListModel.js:4 | +| POST | `.../major/order/table` | views/order/OmdeListModel.js:7 | +| POST | `.../order-tamin` | views/order/TaminListModel.js:3 | +| POST | `.../get-custom-orders` | views/order/makeOrderModel.js:4 | +| GET | `.../major/order` | views/customer/omde/omdeOrder.js:4 | +| GET | `.../single/order` | views/customer/single-customer/singleOrder.js:4 | +| PUT/POST | `order/`, `order/{id}` | store/order/index.js:54,57 | +| GET | `order/` | store/order/index.js:69 | +| DELETE | `order/{id}` | store/order/index.js:42 | +| GET | `/order/type` | views/order/create.vue:395 | +| POST | `single/order-sts/edit` | views/order/List.vue:260, 271 | +| POST | `order-sts/edit` | views/order/TaminList.vue | +| POST | `major/order-sts/edit` | views/order/OmdeList.vue | +| POST | `order-item/edit` | views/order/OmdeList.vue | +| DELETE | `major/order_item/` | views/order/OmdeList.vue | +| PUT | `custom-order/{id}` | views/order/MakeOrder.vue | +| DELETE | `custom-order/{id}` | views/order/MakeOrder.vue | +| POST | `delete-order` | views/order/* | +| *(window.open)* | `.../order/pdf/{id}` | views/order/List.vue:296 | + +### Customers +| Method | Endpoint | Caller | +|---|---|---| +| POST | `.../customer` | views/customer/omde/OmdeListModel.js:7 | +| POST | `.../major/verify/table` | OmdeListNonVerifiedModel.js:7 | +| POST | `.../major/archive/table` | OmdeArchiveListModel.js:7 | +| POST | `.../banker` | BankerListModel.js:7 | +| POST | `.../shopkeeper` | shopkeeperListModel.js:7 | +| POST | `.../reagent` | ReagentListModel.js:7 | +| POST | `.../single-customer` | SingleCustomerList.js:3; views/order/create.vue:397 | +| POST | `.../major/table` | table/major_customer.js:3 | +| GET | `.../major/customer` | omdeCustomer.js:4 | +| POST | `customer/create` | store/customer/index.js:71 | +| GET | `customer/{id}` | bulkCustomer.vue:330; ReagentCustomer.vue:274 | +| POST | `customer/toggle-active` | omde list views | +| POST | `customer/toggle-sts` | omde list views | +| POST | `major/verify` | OmdeNonVerifiedList.vue | +| GET | `major/archive/{id}` | OmdeArchivedList.vue | +| GET | `major/dashboard/{id}` | ViewOmdeCustomer.vue | +| POST | `single` | singleCustomer.vue:92 | +| PUT | `single/{id}` | singleCustomer.vue:90 | +| GET | `single/{id}` | singleCustomer.vue:119 | +| GET | `single/order/{id}` / `single/report/{id}` / `single/wallet/{id}` | ViewSingleCustomer.vue | +| GET | `query/reagent` | bulkCustomer.vue:315 | +| GET/PUT/DELETE/POST | `user/profile[/{id}]` | store/customer/index.js:44,56,59,82,94 | +| GET | `user/profile/address/1` | (grep index) | + +### Finance / documents / wallet / invoices +| Method | Endpoint | Caller | +|---|---|---| +| POST | `.../cash/table` | views/finical/list_model.js:3 | +| GET | `.../major/cash` | omdeTransaction.js:3 | +| GET | `.../single/transaction` | singleTransaction.js:3 | +| POST | `/wallet/table` | views/wallet/walletTbl.js:3 | +| GET | `/wallet/config` | views/manage/wallet.vue:77 | +| GET | `/wallet/birthday/config` | views/manage/wallet.vue:91 | +| POST | `/wallet/config` | views/manage/wallet.vue:109, 126 | +| POST | `.../branch/product/table` | views/invoice/ListInvoice.js:4; views/branch/orderListModel.js:4 | +| POST | `/branch/product` | views/invoice/AddInvoice.vue | +| GET | `branch/product/{id}` | views/invoice/EditInvoice.vue | +| DELETE | `branch/product/{id}` | views/invoice/ListInvoice.vue | + +### Branches / workshops / cities +| Method | Endpoint | Caller | +|---|---|---| +| POST | `.../branch/table` | views/branch/ListBranch.js:3 | +| POST | `/branch` | views/branch/AddBranches.vue | +| PUT | `/branch/{id}` | views/branch/EditBranches.vue | +| GET | `/branch/{id}` | views/branch/EditBranches.vue | +| GET | `/branch/list` | (grep index) | +| GET | `/branch/active/{id}` | views/branch/ListBranches.vue | +| POST | `workshop/table` | views/workshop/ListWorkshop.js:3; views/order/workshopModel.js:3 | +| POST | `.../workshop/product/table` | views/workshop/orderListModel.js:4 | +| POST | `/workshop` | views/workshop/AddWorkshop.vue | +| PUT | `/workshop/{id}` | views/workshop/EditWorkshop.vue | +| GET | `/workshop/{id}` | views/workshop/EditWorkshop.vue | +| GET | `/workshop/active/{id}` | views/workshop/ListWorkshop.vue | +| POST | `cities/list` | store/users/index.js:52; bulkCustomer.vue:321; CityListModel.js:3 | +| POST | `cities` | views/city/createCity.vue | +| PUT | `cities/{id}` | views/city/cityList.vue | +| DELETE | `cities/{id}` | views/city/cityList.vue | +| GET | `cities/get-percent/{id}` | views/city/cityList.vue | +| GET | `cities/get-percent-aux/{id}` | views/city/cityList.vue; bulkCustomer.vue:265; ReagentCustomer.vue:221 | + +### Promotions / offers +| Method | Endpoint | Caller | +|---|---|---| +| POST | `.../major/promotion/table` | src/table/list_model.js:3 | +| GET | `/product/variant/promotion/{id}` | views/promotion/* | +| POST/PUT/DELETE | `product/variant/promotion[/{id}]` | store/promotion/index.js:58,70,73,85 | +| POST/PUT/DELETE | `product/variant/promotion/variant-promotion[/{id}]` | store/variantpromotion/index.js:58,71,75,88 | +| GET | `/autopromotion` | views/promotion/autopromotion.vue | +| PUT | `/autopromotion` | views/promotion/autopromotion.vue | +| GET | `/ignore` | views/offersIgnore/offersIgnore-add-edit.vue | +| POST | `/ignore` | views/offersIgnore/offersIgnore-add-edit.vue | +| DELETE | `/ignore/` | views/offersIgnore/offersIgnore-add-edit.vue | + +### Comments / SMS / notifications / newsletter / reports / config +| Method | Endpoint | Caller | +|---|---|---| +| POST | `.../comment/table` | views/customer/Comments/CommentListModel.js:4 | +| POST | `comment` | Comments.vue | +| POST | `comment/sts` | Comments.vue | +| DELETE | `comment/{id}` | Comments.vue | +| POST | `/sms/table` | views/sms/create.vue:168 | +| POST | `https://henzagold.com/oapi/api/sms/batch` | views/sms/create.vue:216, 231 | +| POST | `https://henzagold.com/oapi/api/sms/batch/test` | views/sms/create.vue:195 | +| GET | `.../major/sms` | omdeSms.js:3 | +| GET | `.../single/sms` | singleSms.js:3 | +| POST | `/notify` | views/notify/create.vue:112 | +| POST | `/notify/table` | views/notify/notifyTbl.js:3 | +| POST | `/notify/customer` | views/notify/create.vue:139 | +| GET | `/notify/{id}` | views/notify/user.vue | +| GET | `/notify/read/{id}` | views/notify/user.vue | +| POST | `.../fashion/table` | views/newsletters/newsletter.js:3 | +| POST | `.../report/order_item` | views/report/order_item_model.js:3 | +| GET | `get-analytics` | views/DashboardECommerce.vue | +| GET | `get-analytics-tak` | views/DashboardAnalytics.vue | +| GET | `/dashboard/link` | views/manage/report.vue:52 | +| POST | `log/{mode}`, `log/customer/{id}` | views/statics/index.vue:147 | +| GET | `log/ago/{id}` | views/statics/index.vue:174 | +| GET | `/log/list/customer` | views/statics/index.vue:188 | +| GET | `config/statics` | store/{product,variant,promotion,variantpromotion,manage}/*.js | +| GET | `/const/__list__ut` | views/user/Permission.vue:113; UserCreate.vue:169; ListUser.vue:257 | +| GET | `/const/__list__tags` | AddProduct.vue:989 | +| GET | `/const/mobile_main_slider`, `/const/site` | views/manage/Const.vue | +| POST | `const`, `/const` | views/user/Permission.vue:77; views/manage/Const.vue | +| PUT | `/const/`, `/const/49`, `/const/54`, `/const/site_test` | views/manage/Const.vue | +| DELETE | `/const/by-id/` | views/manage/Const.vue | +| GET | `price` | components/goldPrice.vue | +| PUT | `file/update/` | upload components | +| POST | `{baseURL}file/create` | input/UploadInput.vue:72; input/SingleUploadInput.vue:52 | +| GET | `/te/query`, `POST /ddf` | scratch/debug endpoints | + +--- + +## Appendix B — Known defects / oddities found while reading + +| # | Issue | Evidence | +|---|---|---| +| 1 | No response interceptor and no token-refresh in the live app; the whole `src/http/requests/**` refresh machinery is unreachable | src/axios.js (no response interceptor); src/main.js:22-23 (import commented) | +| 2 | Request-error interceptor does not return the rejection | src/axios.js:29-31 | +| 3 | `store.state.auth.user` / `.token` are **functions**, so `isLogin` in the guard is always truthy | store/auth/moduleAuth.js:5-10 vs router.js:164 | +| 4 | Header-click sorting inverts direction relative to the sort dropdown (`ascend` → `-`) | XTable.vue:449 vs XTable.vue:342 | +| 5 | `XTbl.hideSearch()` and `addExtraBtn()` both set `_hidePageination` (copy-paste) | XTbl.js:51-59 | +| 6 | Menu URL `/customer/Shopkeeper-list` does not match route `/customer/shopkeeper-list` (case) | navMenuItems.js:90 vs admin.js:550 | +| 7 | `order/List.vue` "ویرایش سفارش" persists nothing — the `axios.put` is commented out | views/order/List.vue:281 | +| 8 | Retail order-status enum lacks `0: معلق` present in the wholesale filter | create.vue:158 vs OmdeListModel.js:23 | +| 9 | Banker / Shopkeeper menu entries have no `permissionId`, so they render for everyone | navMenuItems.js:86-87, 93-94 | +| 10 | Permission keys `persons.create.customer.tak` and `persons.list.sellers.view` are referenced in views but absent from `permissionConst.js` | AddCustomer.vue:142; ReagentList.vue:32 | +| 11 | `/newsletters` route has its `role` commented out → reachable by any authenticated admin | admin.js:851 | +| 12 | Jalali datetime filter uses 12-hour `hh` with no am/pm marker | filters.js:109 | +| 13 | Base URL is hard-coded in `src/axios.js` and again as absolute strings in ~30 table models | axios.js:5; e.g. orderListModel.js:4 | +| 14 | SMS send bypasses the admin base and posts to `/oapi/api/sms/batch` | views/sms/create.vue:216 | +| 15 | Refresh-token payload reads `localStorage.getItem('accessToKen')` (capital K typo) | http/requests/auth/jwt/index.js:61 | +| 16 | `auth_config.json` still holds Auth0 placeholders and is never used | auth_config.json; main.js:36-38 | +| 17 | No gram/weight formatter, no Persian↔English digit conversion, no price input mask anywhere | absent from `src/filters`, `src/utils`, `src/components/input` | +| 18 | Permission deletion in the role editor fires N staggered `setTimeout(…, 500)` deletes — racy for large diffs | views/user/Permission.vue:86 | diff --git a/docs/parity/_inv-ref-pwa.md b/docs/parity/_inv-ref-pwa.md new file mode 100644 index 0000000..2e92451 --- /dev/null +++ b/docs/parity/_inv-ref-pwa.md @@ -0,0 +1,1178 @@ +# Inventory — henza_pwa (reference user app) + +> Scope: `E:\gold bank\henza_pwa`. All `path:line` are relative to `henza_pwa/`. +> Excluded from the file census: `node_modules`, `dist`, `3.0.0`, and two **vendored** libraries checked into `src/components/`: `src/components/DatePicker/DatePicker/**` (forked ant-design-vue `vc-picker` + Jalali locale) and `src/components/JalaliAntDatePicker/**` (forked ant-design-vue `_util`/components). Those are third-party code, not app code. +> +> **Critical framing finding:** this app is **NOT** a consumer "gold bank" (buy gram / sell gram / wallet / deposit / withdraw / transfer / KYC-level gating). It is a **B2B wholesale gold jewelry catalog + ordering app** for jewelers (`مشتری`/`کارشناس فروش`/`بنکدار`). There is **no buy-gold, sell-gold, wallet, deposit, withdraw, transfer, cart-payment, address-book, ticket/support, or KYC-level system** anywhere in the codebase (verified by exhaustive grep of every axios call — see §2.4). The closest analogues are documented per-section and flagged **ABSENT** where the requested feature does not exist. + +--- + +## 1. Stack & versions + +Source: `package.json:1-58`. + +| Concern | Package | Version | Evidence | +|---|---|---|---| +| Framework | `vue` | `^3.2.29` | `package.json:34` | +| Build | `vite` | `^3.2.5` | `package.json:52` | +| Vue plugin | `@vitejs/plugin-vue` | `^2.0.0` | `package.json:42` | +| JSX plugin | `@vitejs/plugin-vue-jsx` | `^3.0.0` | `package.json:43` | +| UI library (primary) | `ant-design-vue` | `^3.2.20` | `package.json:17` | +| UI library (mobile) | `vant` | `^4.0.3` | `package.json:33` | +| Icons | `@ant-design/icons-vue` `^6.1.0`, `feather-icons` `^4.28.0`, `vue-feather` `^2.0.0-rc.1` | — | `package.json:10,21,35` | +| Router | `vue-router` | `^4.0.12` | `package.json:36` | +| Store (Pinia) | `pinia` | `^2.1.4` | `package.json:30` — **only used by one store** (`src/views/report/store/report.ts:9`). All other state is hand-rolled class singletons. | +| HTTP client | `axios` | `^0.25.0` | `package.json:18` | +| Date (Jalali) | `moment-jalaali` `^0.10.0`, `jalaliday` `^2.3.0`, `jalali-dayjs` `0.0.1`, `dayjs` `^1.11.6`, `date-fns` `^2.29.3` | — | `package.json:19,20,24,25,29` | +| Date picker | `vue3-persian-datetime-picker` `^1.2.2` (+ vendored fork under `src/components/DatePicker/`) | — | `package.json:38` | +| PWA plugin | `vite-plugin-pwa` | `^0.11.13` | `package.json:54` | +| Service-worker toolkit | `workbox-precaching` | `^6.4.2` | `package.json:56` | +| **Socket / live-price lib** | **NONE** | — | No `socket.io`, `ws`, `pusher`, `centrifuge`, `sse`, `eventsource` in `package.json`. Live price is **manual pull only** — see §10. | +| Push notifications | `firebase` `^9.6.7`, `@firebase/app` `^0.7.17`, `@firebase/messaging` `^0.9.8` | — | `package.json:11,12,22` — FCM wiring exists but is **disabled** (`src/App.vue:104-106` `useFirebase()` commented out) | +| Animation | `gsap` `^3.9.1` | — | `package.json:23` (used by bottom-nav morph, `src/components/ui/navigation.vue:83`) | +| Lottie | `@lottiefiles/dotlottie-vue` `^0.4.15` | — | `package.json:14` (version-update dialog, `src/App.vue:24`) | +| Carousel | `swiper` `^7.4.1` | — | `package.json:32` | +| Lazy images | `vue3-lazyload` `^0.2.5-beta` | — | `package.json:37` (registered `src/main.ts:32-46`) | +| Utils / composables | `lodash` `^4.17.21`, `@vueuse/core` `^9.13.0` | — | `package.json:28,16` | +| CSS | `tailwindcss` `3.0.18`, `less` `^4.1.2`, `postcss-rtl` `^1.7.3`, `autoprefixer`, `@fullhuman/postcss-purgecss` | — | `package.json:13,26,31,44,50` | +| TS | `typescript` `^4.4.4`, `vue-tsc` `^0.29.8` | — | `package.json:51,55` | +| App version | `1.0.7` | — | `package.json:3` (compared against `localStorage.version` at `src/App.vue:57-68`) | + +--- + +## 2. HTTP layer + +### 2.1 Base URLs & config + +`src/service/` contains a **single** file (`src/service/hesab.ts`) with 5 functions. **Every other API call is inlined in views/stores/mixins.** Full census in §2.4. + +| Key | Value | Evidence | +|---|---|---| +| `$themeConfig.baseURL` | `https://henzagold.com/oapi/api/` | `themeConfig/themeConfig.ts:43` | +| `$themeConfig.coreURL` | `https://henzagold.com/oapi/` | `themeConfig/themeConfig.ts:45` | +| `baseURL` (named export, unused by axios) | `https://henzagold.com/oapi/api/` | `themeConfig/themeConfig.ts:40` | +| `$themeConfig.baseRoute` | `/` | `themeConfig/themeConfig.ts:47` | +| `$themeConfig.outDir` | `dist` | `themeConfig/themeConfig.ts:48` | +| `$BasePath` | dev → `http://localhost:3030`, prod → `https://app.henzagold.com` | `themeConfig/themeConfig.ts:50` | +| Commented-out local alternatives | `http://localhost/api/`, `http://localhost/oapi/` | `themeConfig/themeConfig.ts:44,46` | + +There is **no `.env` / `import.meta.env` usage anywhere** — all config is hard-coded in `themeConfig/themeConfig.ts`. `vite.config.ts:91-93` stubs `process.env` to `{}`. + +### 2.2 Axios instances (`src/plugins/axios.ts`) + +Three instances, all created with the same static default `headers` object built **once at module load** (`src/plugins/axios.ts:6-10`): + +``` +Content-Type: application/json +accept: application/json +Authorization: Bearer ${getUserToken()} // snapshot at import time +``` + +| Export | baseURL | Request interceptor | Evidence | +|---|---|---|---| +| `default` (`ax`, imported everywhere as `@axios`) | `$themeConfig.baseURL` | Overwrites `config.headers.Authorization = 'Bearer ' + getUserToken()` on **every** request | `src/plugins/axios.ts:15-24` | +| `noHeaderAxios` | `$themeConfig.baseURL` | Pass-through (`config => config`) — caller must set `Authorization` manually | `src/plugins/axios.ts:27-32` | +| `cax` | `$themeConfig.coreURL` | Sets `Authorization = 'Bearer ' + localStorage.getItem('appToken')` (ignores child token) | `src/plugins/axios.ts:37-45` — **`cax` is never imported anywhere** (dead export) | + +`@axios` alias → `src/plugins/axios` (`vite.config.ts:61-64`). `@themeConfig` → `themeConfig/themeConfig` (`vite.config.ts:57-60`). `/@/` → `src/` (`vite.config.ts:65-68`). + +### 2.3 Interceptors, token, 401, error toasts + +| Aspect | Reality | Evidence | +|---|---|---| +| **Request interceptor** | Only attaches `Authorization`. Error handler is a **no-op bug**: `error => { Promise.reject(error) }` — result not returned, so the promise resolves `undefined`. | `src/plugins/axios.ts:17-23`, same bug at `:29-31` and `:39-45` | +| **Response interceptor** | **NONE.** No response interceptor is registered on any instance. | `src/plugins/axios.ts` (entire file, 46 lines) | +| **Token source** | `getUserToken() = userChildToken() ?? userToken()` | `src/store/tokenManage.ts:8-10` | +| `userToken()` | `localStorage.getItem('appToken') \|\| localStorage.appToken \|\| null` | `src/store/tokenManage.ts:4-6` | +| `userChildToken()` | `localStorage.getItem('activeChildToken') \|\| localStorage.activeChildToken \|\| null` | `src/store/tokenManage.ts:1-3` | +| **Impersonation** | If a sales-rep selects a customer, that customer's `api_token` is stored as `activeChildToken` and **takes priority** over the rep's own token for all requests. | `src/store/myDataStore.ts:159-169` | +| **Refresh logic** | **ABSENT.** No refresh token, no retry queue, no `/refresh` endpoint anywhere. | verified by grep — no `refresh` API call exists | +| **401 handling** | Handled in **exactly one place**, `myStore.init()`: on `e.response.status === 401` → remove `appToken`, `appCart`, `activeChildToken`; `toggleActiveUser(undefined)`; `router.push({name:'home'})`; then `throw e`. | `src/store/myDataStore.ts:134-145` | +| **403 handling** | **ABSENT.** No `403` reference anywhere. | grep | +| **Global error toast** | **ABSENT.** Every call site does its own `try/catch { console.log(e) }`. Toasts are per-view (`showNotify` / `NotifyVant` / `message.error`). | e.g. `src/views/home/index.vue:218-222`, `src/views/cart.vue:189-193`, `src/Mixins/getProducts.ts:35-37` | +| **Per-call error toasts that DO exist** | `addUser` duplicate-mobile (`src/views/user/src/components/addUser.vue:182-189`), upload errors (`src/components/SingleImageUploader.vue:94-97,117-122,129-139`), login 404/other (`src/views/user/LoginRegister/components/Login.vue:147-154,177-183`), makeOrder (`src/views/user/src/components/makeOrder.vue:224,229`), editUser success (`src/views/user/src/components/editUser.vue:285-291`) | — | + +### 2.4 Complete API function census + +Every network call in the app (excluding the two vendored libs). **58 distinct call sites, 44 distinct endpoints.** + +#### `src/service/hesab.ts` (the only "service" file — 5 functions) + +| # | Function | Method | URL | Params / body | Return type | Evidence | +|---|---|---|---|---|---|---| +| 1 | `hesabDetail(page=1, limit=10)` | POST | `/hesab_detail/table` | body `{limit, page}` | `TableOutput` | `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` | `src/service/hesab.ts:16-18` | +| 3 | `accountingDetail(factor_code?)` | GET | `/fin_tahesab/detail/${factor_code}` | path param | `TableOutput` | `src/service/hesab.ts:20-22` | +| 4 | `accountingDetailList(offset?)` | GET | `/fin_tahesab/my_detail/${offset}` | path param | — | `src/service/hesab.ts:23-25` | +| 5 | `hesabMe()` | GET | `/tahesab/me` | — | `any` | `src/service/hesab.ts:26-28` | + +#### Auth & profile + +| # | Method | URL | Body / params | Called from | Evidence | +|---|---|---|---|---|---| +| 6 | POST | `/web/auth` | `{username}` (phone) | Login step 1 | `src/views/user/LoginRegister/components/Login.vue:137` | +| 7 | POST | `/user-verfiy` *(sic — typo in endpoint)* | `{username, password /*=OTP*/, token_mobile}` | Login step 2 | `src/views/user/LoginRegister/components/Login.vue:165` | +| 8 | POST | `/major/register` | `{fname, lname, mobile, store_name, phone, address, city}` | Register | `src/views/user/LoginRegister/components/Register.vue:332` | +| 9 | POST | `/major/complete` | `{business_license_image_id, shop_image_id, ncode_image_id}` | Complete profile (doc upload) | `src/views/user/src/completeProfile.vue:193` | +| 10 | POST | `/me-data` | `{}` | `myStore.init()` — **called twice**: once via `noHeaderAxios` with explicit parent token, once via `axios` with child token | `src/store/myDataStore.ts:120-126` (noHeaderAxios), `:130` | +| 11 | POST | `/me-data` | `{}` | makeOrderStatus bootstrap | `src/views/user/src/components/makeOrderStatus.vue:180` | +| 12 | POST | `/update-profile` | `{image_id, phone, address, fname, lname, mobile, birth_date, marriage_date}` | editUser | `src/views/user/src/components/editUser.vue:289` | +| 13 | POST | `/cities/list` | `{}` → `{rows: City[]}` (flat list w/ `level`,`parent`) | Register city cascader | `src/views/user/LoginRegister/components/Register.vue:299` | + +#### Catalog / products + +| # | Method | URL | Body / params | Called from | Evidence | +|---|---|---|---|---|---| +| 14 | POST | `/front` | `{}` → `{products, more, cats, ciny}` | Home bootstrap | `src/views/home/index.vue:184` | +| 15 | POST | `/front` | `{}` (for `ciny` fallback list) | `getCollection` in products mixin | `src/Mixins/getProducts.ts:46` | +| 16 | POST | `/get-products` | full `Query` object (see §8) → `{rows, count, limit}` | product list / search / newest | `src/Mixins/getProducts.ts:28` | +| 17 | POST | `/get-products` | `{products_id: number[]}` → `{rows}` | cart re-hydration | `src/store/cartStore.ts:41` | +| 18 | GET | `/ciny/${cats[0]}` | path param = first category id | collection list for a category | `src/Mixins/getProducts.ts:42` | +| 19 | GET | `/product/${id}` | path param (from route) | single product | `src/views/singleProduct.vue:283` | +| 20 | GET | `/product/${props.id}` | path param (from prop, drawer mode) | single product in drawer | `src/views/singleProduct.vue:304` | +| 21 | GET | `/config` | — → `{about, main_slider}` | base store init | `src/store/store.ts:63` | +| 22 | GET | `/promotion` | — (only if logged in) | base store init | `src/store/store.ts:74` | +| 23 | GET | `/ignore` | — (only if logged in) | base store init | `src/store/store.ts:86` | +| 24 | GET | `/ignore` | — | `getCustomerListType()` — **dead**, never called, duplicates #23 | `src/store/store.ts:98` | +| 25 | GET | `/price` | — → keyed object of price rows | base store `getPriceList()` | `src/store/store.ts:114` | +| 26 | GET | `/price` | — → array or object | Dashboard `getListPrice()` (separate, newer path) | `src/views/user/src/components/Dashboard.vue:634` | + +#### Favorites + +| # | Method | URL | Body | Called from | Evidence | +|---|---|---|---|---|---| +| 27 | POST | `/add-to-fav` | `{id}` — **toggle** (optimistic local toggle first) | favorite mixin | `src/Mixins/favorite.ts:8` | +| 28 | POST | `/get-favorites` | `{page}` → `{rows}` | favorites list | `src/views/user/src/components/favorits.vue:52` | + +#### Orders (catalog orders) + +| # | Method | URL | Body | Called from | Evidence | +|---|---|---|---|---|---| +| 29 | POST | `/set-order` | `{final_submit:false, from_parent:boolean, id:null, items:[{discount, id:null, note, percent_pay, product, promotion_id, promotion_item_id, qty, rash_weight, rial_pay:0, variant, wage_weight, weight}]}` → `{id}` | cart checkout | `src/views/cart.vue:161-185` | +| 30 | POST | `/get-orders` | `{sts}` → `{rows}` (`sts = -1` for all) | orders list / tab change | `src/views/user/src/components/Orders.vue:290` | +| 31 | POST | `/order/child` | `{page, search}` → `{rows}` | sales-rep: customers' orders | `src/views/user/src/components/ChildOrders.vue:332` | +| 32 | PUT | `/order/child/${id}` | `{items:[...w, rash_weight], final_submit, id, from_parent:true}` | sales-rep: edit/finalize a customer order | `src/views/user/src/components/ChildOrders.vue:400` | + +#### Custom manufacture orders ("سفارش ساخت") + +| # | Method | URL | Body | Called from | Evidence | +|---|---|---|---|---|---| +| 33 | POST | `/create-custom-order` | `{color, size, color_mina, color_stone, sum, file_id, description}` | makeOrder submit | `src/views/user/src/components/makeOrder.vue:221` | +| 34 | GET | `/get-custom-orders` | — → `{rows}` | makeOrderStatus list | `src/views/user/src/components/makeOrderStatus.vue:187` | +| 35 | POST | `/update-custom-order/${orderId}` | `{sts: 2}` | (commented-out generic updater) | `src/views/user/src/components/makeOrderStatus.vue:197` | +| 36 | POST | `/update-custom-order/${orderId}` | `{sts: 2}` | `confirmOrder` | `src/views/user/src/components/makeOrderStatus.vue:212` | +| 37 | POST | `/update-custom-order/${orderId}` | `{sts: 2}` | `completeOrder` — **same `sts:2` as confirm (likely bug)** | `src/views/user/src/components/makeOrderStatus.vue:234` | + +#### Financial documents ("اسناد" / findoc) + +| # | Method | URL | Body | Called from | Evidence | +|---|---|---|---|---|---| +| 38 | POST | `/get-my-findoc` | `{}` → `{rows}` — **no pagination, no filters** | transactions list | `src/views/user/src/components/transactions.vue:203` | +| 39 | POST | `/set-findoc` | type-dependent (see §6.9) | addTransaction (page) | `src/views/user/src/components/addTransaction.vue:196` | +| 40 | POST | `/set-findoc` | type-dependent (identical) | addFindoc (drawer, duplicate) | `src/views/user/src/components/addFindoc.vue:204` | + +#### Customers (sales-rep only) + +| # | Method | URL | Body | Called from | Evidence | +|---|---|---|---|---|---| +| 41 | POST | `/my-child` | `{}` → customer array | customerChoice list | `src/views/user/src/components/customerChoice.vue:184` | +| 42 | POST | `/customer/toggle-active` | `{id}` | customerChoice activate/deactivate | `src/views/user/src/components/customerChoice.vue:199` | +| 43 | POST | `/customer/toggle-active` | `{id}` | Dashboard `setActive` — **dead** (`getCustomers` is undefined in that scope) | `src/views/user/src/components/Dashboard.vue:593` | +| 44 | POST | `/customer/create` | full `userFormModel` | addUser (create mode) | `src/views/user/src/components/addUser.vue:417` | +| 45 | POST | `/customer/update` | `{...userFormModel, id: route.query.edit}` | addUser (edit mode) | `src/views/user/src/components/addUser.vue:406` | +| 46 | GET | `https://henzagold.com/oapi/admin/major/list_type` | **absolute URL, bypasses baseURL, hits `/admin/`** | addUser: customer-type dropdown | `src/views/user/src/components/addUser.vue:457-460` | + +#### Notifications + +| # | Method | URL | Body | Called from | Evidence | +|---|---|---|---|---|---| +| 47 | POST | `get-notify` *(no leading slash)* | `{page}` → `{rows}` | notification list (infinite scroll) | `src/views/home/src/components/NotificationList.vue:101` | +| 48 | GET | `/notify/count` | — → number | unread badge | `src/views/home/src/components/NotificationList.vue:117` | +| 49 | PUT | `notify/read/${id}` *(no leading slash)* | — | mark read | `src/views/home/src/components/NotificationList.vue:132` | + +> `NotificationList.vue` is **not mounted** — its usage in Home is commented out (`src/views/home/index.vue:5`). + +#### Files + +| # | Method | URL | Body | Called from | Evidence | +|---|---|---|---|---|---| +| 50 | POST | `https://henzagold.com/oapi/api/file/create` | `FormData{file, has_thumb:'0', has_watermark:'0'}`, header `Content-Type: multipart/form-data` → `{id, image}` | SingleImageUploader | `src/components/SingleImageUploader.vue:107-109` | + +#### Analytics (fires on every route change) + +| # | Method | URL | Body | Called from | Evidence | +|---|---|---|---|---|---| +| 51 | POST | `/log/create/${key}` | `{}` or `{data:{product_id}}` when `to.name === 'singleProduct'`; `key = meta.title` + `-${params.id}` | router `beforeEach` | `src/router/customAnalytics.ts:26` | +| 52 | GET | `/log/update/${lastRequestReturnedId}` | — (closes previous page-view) | router `beforeEach` | `src/router/customAnalytics.ts:17` | +| 53 | POST | `/log/create/${routeName}` | — | `useCustomAnalyticsComponent` composable, `onBeforeMount` | `src/router/customAnalytics.ts:40` | +| 54 | GET | `/log/update/${requestReturnedId}` | — | same composable, `onBeforeUnmount` | `src/router/customAnalytics.ts:53` | + +Excluded routes for auto-analytics: `['login', 'userPage::hasSubRoute']` (`src/router/customAnalytics.ts:6`). + +#### Non-axios external URLs (`window.open`) + +| Purpose | URL | Evidence | +|---|---|---| +| Order invoice PDF | `https://henzagold.com/oapi/api/report/order?order_id=${id}` (target `_bank`) | `src/views/user/src/components/Orders.vue:334` | +| Findoc file | `https://henzagold.com/oapi/api/report/doc?doc_id=${id}` (target `_bank`) | `src/views/user/src/components/transactions.vue:225` | +| Phone call | `tel:${mobile}` | `src/views/user/src/components/profile.vue:149-151` (line numbering within `