Files
tehran_tala/docs/PWA-PORT-PLAN.md
T

539 lines
53 KiB
Markdown
Raw Normal View History

2026-10-04 20:11:09 +03:30
# پلن پورت اپ کاربر کلاسیک — henza_pwa → apps/user-app-classic
سند برنامه‌ریزی. **قبل از تأیید شما هیچ کدی نوشته نمی‌شود.**
> **مرجع پین‌شده:** `henza_pwa` در کامیت `b171fff` (2026-09-02، «fee in bonakdar removed»).
> هر ارجاع `path:line` در این سند به همین نسخه است و نسبت به `henza_pwa/` نوشته شده.
> منبع اصلی شواهد: `docs/parity/_inv-ref-pwa.md` (سیاهه‌ی کامل مرجع) به‌علاوه‌ی
> راستی‌آزمایی مستقیم روی کد. اگر مرجع عوض شد، این سند باید دوباره راستی‌آزمایی شود.
---
## خواسته
1. یک اپ کاربر **سوم** در مونوریپو، کنار `user-app` و `user-app-v2`.
2. طراحی آن = **طراحی قبلی همین سیستم**، یعنی `henza_pwa`.
3. همه‌ی صفحه‌ها، کارکردها، عملیات، انیمیشن‌ها و ناوبری **دقیقاً مثل مرجع**.
4. با **تکنولوژی جدید و کد بهتر**.
5. **هیچ چیزی از بین نرود.**
---
## قرارداد «دقیقاً مثل مرجع»
| لایه | قاعده |
|---|---|
| **آنچه کاربر می‌بیند و انجام می‌دهد** | **عیناً مرجع.** چیدمان و ابعاد، رنگ‌ها، فونت، آیکون‌ها، متن‌های فارسی (بایت‌به‌بایت — حتی فاصله‌ی دوتایی بعد از «لطفا» و غلط املایی «کارکتر»)، ارقام، ترتیب و برچسب تب‌ها، پیام‌های اعتبارسنجی، رنگ و مدت toast ها، پارامترهای انیمیشن، مسیرها و نام route ها، رفتار guard ها، محاسبه‌ها و گرد کردن‌ها. |
| **زیر کاپوت** | **بازنویسی‌شده و بهتر.** TypeScript با `strict`، Pinia، composable ها، یک لایه‌ی HTTP تایپ‌شده، تنظیمات از env، حذف کد مرده و تکراری، تست. |
| **کوییرک بصری بی‌خطر** | **عیناً تکرار می‌شود** — مثلاً notify ورود که حتی برای موفقیت قرمز است، یا تشدید سرگردان در «ّکاربر فعال شود؟». |
| **استثنا** | **فقط خرابی‌های کارکردی مرجع، طبق D4.** هر کدام شماره دارد، در §تنش ۴ و §چیزی که پورت نمی‌شود فهرست شده، و هیچ‌کدام بی‌صدا نیست. |
«هیچ چیزی از بین نرود» دو معنا دارد و هر دو تعهد این پلن است:
1. **هیچ صفحه، کارکرد، انیمیشن یا متنی از مرجع جا نمی‌ماند** — با سرشماری خودکار اثبات می‌شود (§معیار تمام شد).
2. **هیچ چیز موجود در مونوریپو آسیب نمی‌بیند** — `user-app`، `user-app-v2` و `admin-panel` دست نمی‌خورند؛ تغییرات `apps/api` فقط افزایشی است.
---
## قواعد حاکم (ارث‌برده از پروژه)
1. `henza_pwa/`، `henza_server/` و `henza_admin/` **فقط خواندنی‌اند.** برای اجرای مرجع یک کپی بیرون از آن‌ها ساخته می‌شود؛ حتی `node_modules` هم داخلشان نصب نمی‌شود.
2. **D4** — رفتار تکرار می‌شود، خرابی نه. هر عدم‌تکرار عمدی در ماتریس: «عمداً تکرار نشد — ISSUE #n».
3. **D2** — چیزی که مرجع ندارد ساخته نمی‌شود. یعنی اپ کلاسیک صفحه‌ی اعلان‌ها، تنظیمات ظاهر و تب پنجم ناوبری را **ندارد**، چون مرجع ندارد.
4. **PROTECTED.md** — `user-app` و `user-app-v2` دست نمی‌خورند. حتی بازساختاردهی (مثلاً بیرون کشیدن کد مشترک) تأیید جدا می‌خواهد.
5. **D13** — سطوح فروشگاه تک‌فروشی (کیف پول، cashback، قیمت محصول طبق D16) دست نمی‌خورند.
6. **D12** — ماتریس فیلد به فیلد، و تست رفت‌وبرگشت برای هر موجودیتی که endpoint نوشتنی تازه می‌گیرد.
7. **D18** — تأیید دائمی برای شکاف‌های بی‌ابهام؛ توقف فقط برای موارد فهرست D18.
8. **T1 / D21** — مشتری همیشه از توکن، هرگز از بدنه یا URL؛ ادعای `typ`؛ تست توکن متقابل.
9. **D25** — خواندن env فقط با `envOr` / `envOrNull`.
10. **D15** — تست‌هایی که ورودی فارسی دارند، ورودی را از فایل UTF-8 می‌خوانند.
> ⚠️ **`CLAUDE.md` در این مخزن نیست.** اسناد parity مدام به §2، §3، §4 و §6 آن ارجاع می‌دهند،
> ولی فایل در ریشه‌ی workspace قبلی بوده. قواعدش از روی همین ارجاع‌ها بازسازی شده:
> §2 منطق اپ‌های کاربر باید یکسان باشد و فقط طراحی فرق کند؛ §3 باگ مرجع تکرار و ثبت شود،
> مگر حفره‌ی امنیتی در جریان پول — آنجا توقف و سؤال؛ §4 PROTECTED فقط افزایشی؛
> §6 هر حذف داده در CHANGELOG ثبت شود.
> **پیشنهاد: نسخه‌ی اصلی را به مخزن برگردانید** تا متن اصلی جای این بازسازی را بگیرد.
---
## دامنه‌ی واقعی کار (اعداد راستی‌آزمایی‌شده)
| سنجه | عدد | نحوه‌ی راستی‌آزمایی |
|---|---|---|
| route | **22** (۲۱ واقعی + catch-all) | `_inv-ref-pwa.md` §3 |
| فایل `.vue` غیر vendored | **77** فایل، **12,445** خط | `find src -name '*.vue'` بدون دو پوشه‌ی DatePicker |
| فایل‌های `views/` | 34 — سه‌تا مرده: `login.vue` (کل فایل کامنت)، `user/src/components/report.vue` (صفحه‌ی دیباگ)، `NotificationList.vue` (سوار نشده) | §6.1 |
| کامپوننت اپ | 40 | §9 |
| store | ۹ singleton دست‌ساز + ۱ Pinia | §7 |
| endpoint یکتا | **44** در ۵۸ محل فراخوانی → **۳۹ ردیف کارکردی زنده** بعد از کنار گذاشتن مرده‌ها | §2.4 + §نقشه‌ی endpoint در همین سند |
| پوشش API هدف از آن ۳۹ ردیف | ✅ **12** · 🟡 **9** · ❌ **18** | §نقشه‌ی endpoint |
| `@keyframes` | 8 | grep |
| محل `<transition>` | ۷ زنده + ۲ کامنت‌شده (از جمله **انتقال بین صفحه‌ها**) | grep |
| GSAP / `useTransition` | ۱ / ۱ — هر دو در ناوبری پایین | `navigation.vue:83,110` |
| overlay | ۲۰ `Drawer`، ۳ `Modal`، ۲ `van-action-sheet` | grep |
| asset | ۲۴ svg، ۹ png، ۱ jpg، ۱ ico، فونت Yekan Bakh FaNum (۸ وزن × ۳ فرمت)، `icons.json` | `find src/assets public` |
| نقص شناخته‌شده‌ی مرجع | 20 | §13 |
---
## «تکنولوژی جدید» — چه چیزی عوض می‌شود و چه چیزی نه
### کتابخانه‌های UI روی نسخه‌ی lockfile مرجع قفل می‌شوند
**یافته:** `yarn.lock` مرجع خودش Vue **3.5.40** را resolve کرده. پس «تکنولوژی جدید» در
کتابخانه‌های UI معنایی ندارد — و نباید داشته باشد، چون ظاهر پیکسلی دقیقاً به همین نسخه‌ها
وابسته است. مثلاً ant-design-vue 4 استایل را به CSS-in-JS برده و ظاهر عوض می‌شود.
| بسته | نسخه‌ی دقیق (بدون `^`) |
|---|---|
| `ant-design-vue` | 3.2.20 |
| `vant` | 4.10.0 — v1/v2 روی 4.10.2 هستند؛ اپ کلاسیک نسخه‌ی مرجع را می‌گیرد |
| `swiper` | 7.4.1 |
| `gsap` | 3.15.0 |
| `tailwindcss` | 3.0.18 |
| `@vueuse/core` | 9.13.0 |
| `moment-jalaali` | 0.10.4 |
| `@lottiefiles/dotlottie-vue` | 0.4.18 |
| `vue3-lazyload` | 0.2.5-beta |
| `vue` / `vue-router` | 3.5.x / 4.x — همان خانواده‌ی lockfile |
**ارقام فارسی از فونت می‌آیند، نه از تبدیل.** مرجع هیچ تبدیل رقمی ندارد (§8.1)؛ ارقام
به‌خاطر فونت FaNum فارسی دیده می‌شوند. اپ کلاسیک هم تبدیل رقم اضافه نمی‌کند.
### آنچه واقعاً نو می‌شود
| مرجع | اپ کلاسیک | چرا |
|---|---|---|
| Vite 3 + plugin-vue 2، TS 4.4 بدون strict، vue-tsc 0.29 | Vite 5، TS 5 با `strict`، vue-tsc 2 — همان زنجیره‌ی v1/v2 | ابزار نگهداری‌شده؛ خطا در زمان build |
| axios 0.25 با سه instance؛ interceptor خطا promise را برنمی‌گرداند؛ response interceptor ندارد | یک client تایپ‌شده؛ توکن در هر درخواست؛ رفتار 401 مرجع (`myStore.init()`) به‌صورت سراسری | نقص #1 و #2 |
| ۹ singleton کلاسی دست‌ساز | Pinia — **با همان شکل state و همان getter های readonly** | یکدستی با v1/v2 |
| Mixin ها (`getProducts`، `product`، `favorite`، ...) | composable های تایپ‌شده با تست واحد | قابل تست |
| URL و کلید هاردکد در ۵ فایل | متغیرهای `VITE_*` | پیکربندی محیط‌ها |
| دو درخت vendored از DatePicker (fork از ant-design-vue) | `generatePicker` عمومی ant-design-vue 3 با config جلالی — **به شرط اثبات ظاهر یکسان در Phase 0** | حذف هزاران خط کد شخص ثالث |
| ۵ کتابخانه‌ی تاریخ | `moment-jalaali` برای فرمت‌ها (خروجی یکسان) + dayjs/jalaliday فقط برای picker | کمتر، با تست خروجی |
| ۴ کامپوننت تقریباً یکسان `ScrollFetch*` | یک کامپوننت با prop | تکرار |
| `addTransaction.vue` و `addFindoc.vue` تکراری | یک فرم، دو پوسته (صفحه و drawer) | تکرار |
| کد مرده (§چیزی که پورت نمی‌شود) | پورت نمی‌شود | — |
| PWA در `mode: "development"` | production | نقص #17 |
| بدون تست | تست واحد (adapter، formatter، validator، موتور تخفیف) + e2e و مقایسه‌ی تصویری با Playwright | اثبات «دقیقاً» |
---
## تنش‌هایی که قبل از شروع باید تصمیمشان را بدانید
### تنش ۱ — ۱۸ endpoint مرجع در API هدف نیست
از ۳۹ ردیف زنده، **۱۸ تا در هدف معادل ندارند و ۹ تا ناقص‌اند** (§نقشه‌ی endpoint). بدون آن‌ها
«دقیقاً مثل مرجع» ممکن نیست. وضعیت امروز v1/v2 این را نشان می‌دهد:
- ثبت‌نام **هیچ چیزی ذخیره نمی‌کند** — کامنت خود `RegisterView.vue:17` این را می‌گوید.
- علاقه‌مندی‌ها **فقط محلی‌اند** (`favoritesStore.ts:1-3`).
- ثبت سند، سفارش ساخت، مدیریت مشتری توسط کارشناس، ویرایش سفارش مشتری، آنالیتیکس و فاکتور اصلاً وجود ندارند.
ساختن این‌ها در `apps/api` افزایشی است (route و جدول/ستون جدید). **ولی با `CLAUDE.md` §2
تعارض دارد:** منطق اپ‌های کاربر باید یکسان باشد. به محض اینکه اپ کلاسیک ثبت‌نام و
علاقه‌مندی واقعی داشته باشد، v1 و v2 عقب می‌افتند.
**پیشنهاد:** بک‌اند **یک بار**، افزایشی و تست‌شده ساخته شود. وصل کردن v1/v2 به آن یک کار
جدا و ثبت‌شده باشد — نه بخشی از این پروژه. **تصمیم شما لازم است** که §2 شامل اپ کلاسیک
هم می‌شود یا نه.
### تنش ۲ — «انتخاب مشتری»: کار کردن به‌جای مشتری
**در مرجع:** کارشناس فروش (type 1) روی یک مشتری می‌زند → `toggleActiveUser` توکن خود آن
مشتری را به‌عنوان `activeChildToken` ذخیره می‌کند، و این توکن **روی همه‌ی درخواست‌ها
اولویت دارد** (`store/tokenManage.ts:8-10`، `store/myDataStore.ts:159-169`). از آن لحظه کل اپ
به‌جای مشتری کار می‌کند: میز کار موجودی مشتری را نشان می‌دهد، سفارش سبد روی حساب مشتری
با `from_parent: true` ثبت می‌شود تا پیامک به کارشناس برسد (`views/cart.vue:162-164`)،
و دکمه به «خروج از حساب مشتری» تبدیل می‌شود.
**در هدف:** D21 **سازوکار** را رد کرد — ساختن توکن مشتری برای کارشناس یک ابزار ربودن نشست
است. `CustomerChoiceView` در v1 با لمس کارت فقط یک drawer جزئیات باز می‌کند
(`openDetail`). پس **خود این قابلیت** امروز در هدف وجود ندارد.
**پیشنهاد:** همان قابلیت، با سازوکار امن:
- توکن خود کارشناس + هدر `X-Acting-As: <customerId>`.
- سرور در **هر درخواست** چک می‌کند `referredByAgentId` آن مشتری = همین کارشناس.
- موضوع همه‌ی route های مشتری = آن مشتری.
- سفارش با پرچم `from_parent` ثبت می‌شود.
- هر تعویض در لاگ ثبت می‌شود.
- **هیچ توکنی ساخته نمی‌شود** و سمت کلاینت فقط شناسه‌ی مشتری انتخاب‌شده نگه داشته می‌شود.
**تأیید شما لازم است**، چون امنیت و جریان پول است (`CLAUDE.md` §3). این کار ریسک REL-03 را هم
بالاتر می‌برد — §پیش‌نیازها.
### تنش ۳ — نقش کارگاه (type 5) در اپ
**در مرجع:** سرپرست کارگاه یک ردیف مشتری با type 5 است. کاشی «سفارش ساخت» برایش مستقیم به
صفحه‌ی وضعیت می‌رود و دکمه‌های «تایید سفارش» و «ارسال سفارش» را می‌بیند
(`makeOrderStatus.vue:68-75`).
**در هدف دو نامزد هست:**
- `AdminUser` با نقش `WORKSHOP_SUPERVISOR` — `Workshop.supervisorId` به اینجا اشاره می‌کند و `PreOrder.workshopId` به `Workshop`.
- `Customer` با `kind = WORKSHOP_SUPERVISOR` — این مقدار در `CustomerKind` هست و برخلاف `SALES_AGENT` منسوخ اعلام نشده.
D21 فقط به `SALES_EXPERT` اجازه‌ی ورود به اپ‌ها را داده.
**پیشنهاد:** `AdminUser`، چون مسیر داده‌ای تمیز همین است. الگوی D21 به `WORKSHOP_SUPERVISOR`
گسترش پیدا کند، با دامنه‌ی محدود به `PreOrder` های کارگاه خودش. **تصمیم شما لازم است**،
از جمله اینکه مشتری با `kind = WORKSHOP_SUPERVISOR` در این صورت چه نقشی دارد.
### تنش ۴ — خرابی‌های مرجع که به منطق کسب‌وکار می‌خورند
D18 برای این‌ها توقف می‌خواهد (مورد ۴). شماره‌ها از `_inv-ref-pwa.md` §13 است.
| # | خرابی در مرجع | اثر برای کاربر | پیشنهاد من |
|---|---|---|---|
| 4 | دکمه‌ی ثبت در صفحه‌ی `addTransaction` بیرون از `<Form>` است و `html-type="submit"` ندارد (`addTransaction.vue:84-97`) | ثبت سند از **صفحه** کار نمی‌کند؛ فقط از drawer | درست شود — یک فرم مشترک، هر دو پوسته کار کنند |
| 5 | کاشی «سفارش مشتریان» تابع را صدا نمی‌زند (`Dashboard.vue:119`) | کارشناس از میز کار به آن صفحه نمی‌رسد | درست شود — D3 خود صفحه را خواسته |
| 6 | کادرهای ته‌حساب طلایی و ریالی اندیس‌ها و formatter ها را جابه‌جا خوانده‌اند (`Dashboard.vue:182-192`) | موجودی طلا و ریال در کادر اشتباه و با قالب اشتباه | هر کادر مقدار درست را با قالب متناسب با برچسبش نشان دهد |
| 8 | دکمه‌ی به‌روزرسانی قیمت، فهرستی را تازه می‌کند که نمایش داده نمی‌شود (`store.ts:107` در برابر `Dashboard.vue:632`) | به‌روزرسانی بی‌اثر | **قبلاً تصمیم گرفته شده (D4):** فهرست دیده‌شده تازه شود |
| 9 | «ارسال سفارش» همان `{sts: 2}` «تایید سفارش» را می‌فرستد (`makeOrderStatus.vue:212,234`) | ارسال در سرور ثبت نمی‌شود | تایید → «تایید کارگاه»، ارسال → «ساخته شده آماده ارسال». مقدار دقیق `PreOrderStatus` را شما تعیین کنید |
| 12 | radio «حواله به» **اندیس** آرایه را می‌فرستد نه متن مقصد (`addTransaction.vue:50`). سرور مرجع کل بدنه را بی‌تغییر ذخیره می‌کرد (`ApiMobileController::setFindoc`) | مقصد حواله به‌صورت عدد ذخیره می‌شد | سرور اندیس را به متن مقصد نگاشت کند؛ UI بی‌تغییر |
| 14 | در `editUser` سه آپلودر مدرک شناسه را دور می‌ریزند و دو تا یک `fileList` مشترک دارند (`editUser.vue:65,69`) | آپلود «موفق» دیده می‌شود ولی چیزی ذخیره نمی‌شود | **عیناً تکرار شود و ثبت شود** — وصل کردنش داده‌ی احراز را از صفحه‌ای عوض می‌کند که مرجع هرگز اجازه نداده بود |
| 15 | شرط infinite scroll گزارش `tableData.value.count` را می‌خواند که هرگز مقدار نمی‌گیرد (`report/index.vue:234`) | فقط صفحه‌ی اول گزارش دیده می‌شود | درست شود — endpoint های D24 صفحه‌بندی دارند |
| 19 | پسوند ` تومان ` روی فیلدی به نام `rial_remain` (`filters/index.ts:13`)؛ ستون هدف `fiatDebtRial` به ریال است | اگر واحدها نخوانند، بدهی ده برابر کم یا زیاد خوانده می‌شود | اول در `henza_server` واحد واقعی `rial_remain` معلوم شود؛ بعد adapter **همان عدد و برچسبی** را نشان دهد که مرجع برای همان بدهی نشان می‌داد |
| — | `singleProduct` دکمه‌ی back را می‌رباید: `history.pushState` + `window.onpopstate = () => history.go(1)`، سراسری و هرگز برداشته نمی‌شود (`singleProduct.vue:376-379`) | بعد از دیدن اولین محصول، back مرورگر یا گوشی **در کل اپ** از کار می‌افتد | محدود به خود صفحه‌ی محصول — نیت ظاهری کد همین است، نشتش به کل اپ خرابی است |
### تنش ۵ — سرور هدف از مرجع سخت‌گیرتر است
سرور مرجع تقریباً هر چیزی را قبول می‌کرد. سرور هدف بیشتر رد می‌کند: قاعده‌ی موجودی و
پیش‌سفارش (PROTECTED §C.1)، MON-05 وقتی اجرا شود، DTO های اعتبارسنجی، قیدهای یکتایی.
ولی UI مرجع در بیشتر خطاها **هیچ چیزی نشان نمی‌دهد** — فقط `console.log(e)`، مثلاً در
`Register.vue:340-342`، `cart.vue:189-191` و ثبت سند. پس درخواستی که مرجع بی‌صدا قبول می‌کرد،
در اپ کلاسیک بی‌صدا شکست می‌خورد و کاربر گیج می‌شود.
| مورد | پیشنهاد من |
|---|---|
| **سقف موجودی:** سبد مرجع سقف ندارد؛ قاعده‌ی موجودی هدف (`capQty`/`maxQty` در v1) وجود دارد | سقف **بی‌صدا** در دکمه‌ی `+` اعمال شود — بدون هیچ عنصر یا متن تازه |
| **خطاهای بی‌صدا** | هر جا مرجع بی‌صدا بوده بی‌صدا بماند، **مگر** جایی که سرور هدف ردی می‌کند که مرجع هرگز نداشت. آنجا نزدیک‌ترین پیام **موجود** مرجع نشان داده شود (مثلاً «خطا در ثبت سفارش» از `makeOrder.vue:229`). هیچ متن تازه‌ای ساخته نمی‌شود |
### تصمیم‌های کوچک‌تر
| # | سؤال | پیشنهاد من |
|---|---|---|
| Q1 | نام، مسیر و پورت اپ | `apps/user-app-classic`، پورت **5183** (`USER_APP_CLASSIC_PORT`)، سرویس docker با همین نام. «classic» و نه «v3»، چون v3 «جدیدترین» را القا می‌کند در حالی که این قدیمی‌ترین طراحی است |
| Q2 | کد مشترک بین سه اپ | فعلاً داخل خود اپ در `src/core/` (api، adapter، قواعد دامنه)، با مرز تمیز تا بعداً قابل استخراج باشد. پکیج مشترک یعنی بازساختاردهی v1/v2 → PROTECTED → تصمیم جدا |
| Q3 | فیلتر رنگ در جست‌وجو — هدف اصلاً داده‌ی رنگ ندارد (`Product` و `ProductVariant` فقط `brand` دارند) | مدل افزایشی `Color` و ارتباطش با variant؛ ویرایشش در پنل ادمین کار جداگانه است. تا آن موقع فهرست خالی — همان چیزی که مرجع با داده‌ی خالی نشان می‌داد |
| Q4 | تابلوی قیمت میز کار: مرجع ۱۴ ردیف دارد (۸ طلا و سکه + ۶ ارز و انس، `store.ts:118-119`)؛ هدف فقط نرخ طلا به ازای عیار | ردیف‌هایی که هدف داده‌ی واقعی برایشان دارد نمایش داده شوند؛ بقیه حذف (قاعده‌ی صداقت، مثل REP-01) تا منبع قیمت تأیید شود. منبع خارجی = تصمیم شما |
| Q5 | ترتیب دسته‌های خانه: آرایه‌ی هاردکد `[37, 45, 31, 32, 42, 26, 36, 35, 34]` با شناسه‌ی عددی legacy (`home/index.vue:189-204`)؛ شناسه‌ی `Category` در هدف cuid است و نه legacyId دارد نه ستون ترتیب | ستون افزایشی ترتیب روی `Category` و پر کردنش از نگاشت مهاجرت. پر کردن = تغییر داده‌ی موجود → D18 توقف #5 → تأیید لازم |
| Q6 | آنالیتیکس مهمان: مرجع برای مهمان هم لاگ می‌فرستد؛ `CustomerActivityLog.customerId` اجباری است و ستون پایان ندارد | فقط کاربر واردشده لاگ شود؛ ستون افزایشی `endedAt` برای معادل `log/update` |
| Q7 | اعلان‌ها، FCM و پرکردن خودکار OTP — هر سه در مرجع سوار نیستند یا غیرفعال‌اند | ساخته نمی‌شوند — دقیقاً مثل مرجع |
| Q8 | اجرای مرجع برای مقایسه‌ی تصویری | کپی در `.parity/ref-pwa/` (gitignored) و رهگیری شبکه با Playwright؛ خود مرجع دست نمی‌خورد |
| Q9 | وضعیت D19 (حالت تحویل) | این پلن فرض می‌کند D19 تمام شده و D18 برقرار است |
---
## پیش‌نیازهای باز از کارهای قبلی
| مورد | چرا این اپ به آن نیاز دارد | Batch |
|---|---|---|
| **D9 قدم ۵** — جابه‌جایی `filterPercentPay` به اندازه‌ی آفست مشتری | اسلایدر اجرت در فیلتر جست‌وجو | B |
| **شکاف یکتایی شماره بین `Customer` و `AdminUser`** — هر چهار بررسی تداخل فقط `adminUser` را چک می‌کنند؛ در `ISSUES.md` ثبت نشده | ثبت‌نام و «افزودن کاربر» هر دو مشتری می‌سازند | A، F |
| **MON-08** — سرور اجرت و تخفیف را خودش بسازد | ثبت سفارش از سبد | E |
| **MON-05** — اجرای `canPlaceOrders` و `verificationStep` | مرجع فقط در کلاینت چک می‌کند (`sts_verify_step_two === 2`) | E |
| **REL-03** — مقدار جدای `JWT_CUSTOMER_SECRET` | کار به‌جای مشتری (تنش ۲) ریسک آن را بیشتر می‌کند | قبل از F |
---
## نقشه‌ی endpoint ها — مرجع ↔ هدف
✅ موجود (فقط adapter شکل) · 🟡 ناقص · ❌ وجود ندارد
| # | مرجع | هدف امروز | وضعیت | Batch |
|---|---|---|---|---|
| 1 | `POST /web/auth` | `POST /customer/auth/request-otp` | ✅ | A |
| 2 | `POST /user-verfiy` | `POST /customer/auth/verify-otp` | ✅ — رد ورود برای `sts_verify_step_one == 3` بررسی شود | A |
| 3 | `POST /major/register` | — | ❌ | A |
| 4 | `POST /major/complete` | `POST /customer/me/complete-profile` | ✅ (D22) | A |
| 5 | `POST file/create` | `POST /customer/me/documents` | ✅ با `purpose` | A، D، G |
| 6 | `POST /me-data` | `GET /customer/me` | 🟡 هیچ‌کدام از `filterBrand`، `filterColor`، `filterGram`، `filterPercentPay`، `favorits`، `to`، `reagent`، `grams_sell`، `mcode` را ندارد | Phase 2 |
| 7 | `POST /front` | `GET /catalog/products` | 🟡 دسته‌ها، `more` و `ciny` آماده نیستند | B |
| 8 | `POST /get-products` (Query) | `GET /catalog/products` | 🟡 فیلتر و صفحه‌بندی سمت سرور ندارد (STUB-03) | B |
| 9 | `POST /get-products` (`products_id`) | `GET /catalog/products` | 🟡 | B، E |
| 10 | `GET /ciny/{cat}` | داخل catalog (`collections`) | 🟡 | B |
| 11 | `GET /product/{id}` | — فقط فهرست | 🟡 | B |
| 12 | `GET /config` | `GET /home/slider` | 🟡 `about` ندارد | B |
| 13 | `GET /promotion` | — | ❌ | B |
| 14 | `GET /ignore` | — | ❌ | B |
| 15 | `GET /price` | `GET /pricing/latest` | 🟡 فقط طلا به ازای عیار (Q4) | C |
| 16 | `GET /tahesab/me` | `GET /customer/me/report/balances` | ✅ (D24) | C |
| 17 | `POST /add-to-fav` | — | ❌ (v1/v2 محلی) | D |
| 18 | `POST /get-favorites` | — | ❌ | D |
| 19 | `POST /get-orders` | `GET /customer/me/orders` | ✅ فیلتر `sts` در کلاینت | D |
| 20 | `POST /update-profile` | — | ❌ | D |
| 21 | `POST /get-my-findoc` | `GET /customer/me/ledger-entries` | 🟡 شکل متفاوت | D |
| 22 | `POST /set-findoc` | — | ❌ | D |
| 23 | `GET /fin_tahesab/table` | `GET /customer/me/report` | ✅ (D24) | D |
| 24 | `GET /fin_tahesab/my_detail/{offset}` | `GET /customer/me/report/detail` | ✅ | D |
| 25 | `GET /fin_tahesab/detail/{code}` | `GET /customer/me/report/:id` | ✅ | D |
| 26 | `report/order?order_id` — فاکتور با `window.open` | — | ❌ | D |
| 27 | `report/doc?doc_id` — فایل سند | — | ❌ | D |
| 28 | `POST /set-order` | `POST /orders` | ✅ (MON-08) | E |
| 29 | `POST /my-child` | `POST /expert/my-customers` | ✅ | F |
| 30 | `POST /customer/toggle-active` | — | ❌ | F |
| 31 | `POST /customer/create` | — | ❌ | F |
| 32 | `POST /customer/update` | — | ❌ | F |
| 33 | `GET admin/major/list_type` | — | ❌ از `CustomerKind` ساخته می‌شود | F |
| 34 | `POST /order/child` | `POST /expert/child-orders` | ✅ | F |
| 35 | `PUT /order/child/{id}` | — | ❌ | F |
| 36 | `POST /create-custom-order` | — مدل `PreOrder` فقط در ادمین | ❌ | G |
| 37 | `GET /get-custom-orders` | — | ❌ | G |
| 38 | `POST /update-custom-order/{id}` | — | ❌ | G |
| 39 | `POST /log/create/{key}` + `GET /log/update/{id}` | — | ❌ | G |
**ساخته نمی‌شوند (در مرجع مرده‌اند):** `POST /hesab_detail/table` (هرگز صدا زده نمی‌شود)،
تکرار `GET /ignore` در `getCustomerListType`، تکرار مرده‌ی `toggle-active` در Dashboard،
`POST /cities/list` (داده گرفته می‌شود ولی UI آبشاری کامنت است و فیلد شهر متن آزاد است —
`Register.vue:58-64`)، و سه endpoint اعلان (کامپوننتشان سوار نیست).
---
## مشخصات حس و حرکت — انیمیشن‌ها و ناوبری
این بخش همان چیزی است که یک پورت سرسری از دست می‌دهد. هر ردیف در Phase 0 با ضبط frame
اثبات می‌شود.
### انیمیشن‌ها
| # | انیمیشن | پارامتر دقیق | محرک | شاهد |
|---|---|---|---|---|
| 1 | فرورفتگی (notch) ناوبری پایین | `gsap.to('.from-path', {duration: .3, attr: {d: draw()}})` | تغییر تب | `components/ui/navigation.vue:50-86` |
| 2 | جابه‌جایی آیکون فعال | `useTransition` با `TransitionPresets.easeInOutCubic`، ‏150ms | تغییر تب | `navigation.vue:110-117` |
| 3 | **shutter هدر** | `@keyframes shutter` در 1s با `cubic-bezier(0.33, 1, 0.68, 1)`؛ `max-height` از 0 تا 120px و `opacity` از 0 تا 1 | **هر تغییر route**: `afterEach` پرچم را false و بعد از 100ms دوباره true می‌کند | `components/appHeader.vue:1-45`، `router/index.ts:273-286` |
| 4 | فاصله‌ی محتوا از هدر | `margin-top = headerHeight` وقتی `transitionEnded` | همراه ۳ | `components/content.vue:2` |
| 5 | **انتقال بین صفحه‌ها** | **ندارد.** `<transition>` در `App.vue:6,17` کامنت است. CSS `slideDown` (`App.vue:151-166`) و `setPgaTransition` زنده‌اند ولی به هیچ چیز وصل نیستند | — | **اضافه نمی‌شود** |
| 6 | باز شدن بخش فیلتر | `@keyframes drop` با `mode="out-in" appear` | باز و بسته کردن | `components/dropDown.vue:11,40` |
| 7 | تعویض تصویر گالری | `slideUp` با `mode="out-in"` | تغییر تصویر یا تنوع | `views/singleProduct.vue:28`، `components/slider/slider.vue:22` |
| 8 | اسپینر | `@keyframes rotate` و `rotateOpacity` (سه حلقه) | بارگذاری | `components/spinner.vue:104,115` |
| 9 | ریل سفارش ساخت | `@keyframes gradientMove` و `colorChange` روی رابط مرحله‌ی جاری | نمایش وضعیت | `makeOrderStatus.vue:283,295` |
| 10 | دیالوگ‌ها و notify | قواعد `animation:` | باز شدن | `DialogVant.vue`، `DialogVantOneButton.vue`، `NotifyVant.vue` |
| 11 | فهرست اسناد | `TransitionGroup name="list"` + کوییرک `testBind` (#13) | بارگذاری فهرست | `transactions.vue:17,177-205` |
| 12 | notify قدیمی | `@keyframes notify-in` | — | `customNotification.vue:47`، `Notification.vue:81` — **اول بررسی شود سوار هستند یا نه** |
| 13 | اسلایدر خانه | Swiper با `loop` و `autoplay` | خودکار | `slider/homeSlider.vue:7,10` |
| 14 | اسلایدر محصول | Swiper با ماژول `Zoom`، بدون loop | لمس | `slider/slider.vue:43-45` |
| 15 | overlay ها | انیمیشن خود کتابخانه برای ۲۰ Drawer، ۳ Modal و ۲ action-sheet | — | با قفل نسخه‌ها تضمین می‌شود |
| 16 | دیالوگ نسخه‌ی جدید | Lottie | تغییر `version` | `App.vue:22-38` |
### زمان‌بندی‌ها
| چه چیزی | مقدار | شاهد |
|---|---|---|
| دیالوگ تشویق به ورود در خانه | 5 ثانیه، **در هر بازدید** — گارد «فقط یک بار» کامنت شده | `home/index.vue:210-216` |
| شمارش معکوس OTP | 120 ثانیه، با صفر پیشرو زیر 10 | `Login.vue:122-131,187-189` |
| toast ها | 4000ms | `Dashboard.vue:269-284` و بقیه |
| debounce جست‌وجو | 700ms | `components/search/SearchInput.vue` |
| بازگرداندن پرچم انتقال هدر | 100ms | `router/index.ts:273-286` |
### ناوبری
- **۲۲ route با همان path، name و meta**، از جمله حروف و نام‌های عجیب: `/ChildOrder`، `/customerChoice`، `/addTransaction`، `/makeOrder_status`، `/complete_profile`، `userPage::hasSubRoute`. خانه `/home` است و `/` به آن redirect می‌شود.
- **`beforeEach`** همان شش قدم `_inv-ref-pwa.md` §5.1، با تصمیم‌های D3:
- مهمان روی پنج route محافظت‌شده → toast «ابتدا وارد شوید» + خانه.
- کاربر واردشده روی `/login` یا `/` → خانه.
- تله‌ی cold-boot (`isLoaded`) — D3 گفته تکرار شود.
- `completeProfile` بسته است وقتی `sts_verify_step_two ∈ {1, 2}`.
- route هایی که مرجع در guard ندارد (`addTransaction`، `makeOrderList`، `makeOrderStatus`، `addUser`، `editUser`، `ChildOrder`، `customerChoice`، و `report`/`reportDetail` که خودشان redirect می‌کنند) در اپ کلاسیک هم در guard نیستند؛ سرور هدف به‌هرحال 401 می‌دهد.
- **ناوبری پایین:** چهار تب به ترتیب index — کاربر (۱)، سبد (۲)، جست‌وجو (۳)، خانه (۴)؛ پیش‌فرض فعال ۴. فقط روی همین چهار route دیده می‌شود، و وقتی پنل فیلتر باز است پنهان می‌شود.
- **`keep-alive` + `suspense` با کلید `route.name`** — هر صفحه‌ی دیده‌شده mount می‌ماند (مثلاً نتایج جست‌وجو بعد از برگشت حفظ می‌شوند). این بخشی از حس ناوبری مرجع است و حفظ می‌شود.
- `scrollBehavior`: بازگرداندن `savedPosition`، وگرنه بالای صفحه.
- layout `FullPage` (بدون ناوبری پایین) برای همان ۸ route مرجع؛ بقیه `Master`.
- ربودن دکمه‌ی back در صفحه‌ی محصول — تنش ۴.
---
## ساختار فازها
### Phase 0 — تصمیم‌ها و زیرساخت اثبات (بدون کد اپ)
1. پاسخ شما به تنش‌های ۱ تا ۵ و Q1 تا Q9.
2. **اجرای مرجع:** کپی `henza_pwa@b171fff` در `.parity/ref-pwa/` (gitignored)، `yarn install --frozen-lockfile`، اجرا روی پورت 3030. اگر toolchain قدیمی روی Node فعلی بالا نیاید → ریسک R1.
3. **یک مجموعه‌داده با دو خروجی:** JSON به شکل API مرجع (برای رهگیری درخواست‌های `https://henzagold.com/oapi/**` در Playwright) و seed دیتابیس برای API هدف. یک منبع، دو projection — تا هر دو اپ دقیقاً یک داده را نشان دهند.
4. **پرسوناها:** مهمان؛ مشتری تأییدنشده؛ مشتری تأییدشده (`sts_verify_step_two = 2`)؛ بنکدار (type 4)؛ کارشناس فروش (type 1)، و کارشناس در حال کار به‌جای مشتری؛ کارگاه (type 5).
5. **مقایسه‌ی تصویری:** Playwright روی یک ماشین و یک مرورگر برای هر دو اپ؛ viewport های 390×844 و 768×1024 (کانتینر مرجع `max-w-screen-md` است)؛ نواحی پویا مثل تاریخ و تایمر ماسک می‌شوند. آستانه‌ی قبول همین‌جا توافق می‌شود.
6. **بررسی انیمیشن:** ضبط frame در زمان‌های ثابت پس از محرک (مثلاً 0، 150، 300 و 1000ms) برای هر ردیف جدول انیمیشن.
7. **spike تقویم جلالی:** `generatePicker` عمومی + config جلالی کنار fork مرجع. اگر مقایسه‌ی تصویری صفر نشد، fork به‌صورت vendored نگه داشته می‌شود.
8. ساخت `docs/parity/03-PWA-CLASSIC-MATRIX.md` — فیلد به فیلد طبق D12.
🛑 **Gate — توقف و تأیید شما.**
### Phase 1 — پایه‌ی `apps/user-app-classic`
- اسکلت Vite 5 + Vue 3.5 + TS strict + Pinia + vue-router، با نسخه‌های قفل‌شده‌ی بالا.
- **asset ها بایت‌به‌بایت با checksum:** فونت‌ها، `icons.json` و `Icon.vue`، تصاویر `assets/img` و empty، favicon، آیکون‌های manifest، فایل Lottie، `whatsapp.png`.
- `themeConfig/colors.json`، متغیرهای less، `tailwind.config.js` و `postcss-rtl` — همان مقادیر.
2026-10-10 17:17:14 +03:30
- `index.html` همان: عنوان «طلای منتزر»، `theme-color` ‏`#1C2D56`، قفل zoom، دکمه‌ی شناور واتس‌اپ (شماره از env، با مقدار پیش‌فرض مرجع). `bale.png` در `public/` هست ولی در `index.html` استفاده نمی‌شود — بررسی می‌شود.
2026-10-04 20:11:09 +03:30
- PWA: `injectManifest`، `sw.ts` فقط precache، manifest همان، **mode production**.
- زیرساخت: پورت 5183، `Dockerfile` و nginx به الگوی v1، سرویس compose، اضافه شدن به `build:release`، به‌روزرسانی `DELIVERY.md`.
### Phase 2 — لایه‌ی core
- **client HTTP:** base از env، توکن در هر درخواست؛ 401 سراسری = همان کار `myStore.init()` مرجع (پاک کردن `appToken`، `appCart` و کلید acting-as، رفتن به خانه).
- **adapter ها — قلب کار:** DTO هدف → شکل‌هایی که view های مرجع انتظار دارند (`MyData`، `Product`، `Variant`، `cartItem`، سفارش با `sts` عددی، ردیف سند، ردیف گزارش). view ها با همان نام فیلدهای مرجع bind می‌شوند. **قاعده‌ی صداقت:** چیزی که هدف ندارد `null` می‌شود و ردیفش پنهان — هرگز مقدار ساختگی.
- **نگاشت enum ها:**
- `CustomerKind` → `type` مرجع (۲، ۳، ۴، ۵؛ و ۱ برای کارشناس).
- `OrderStatus` هدف (۹ وضعیت) → `sts` مرجع (۸ تب).
- `PreOrderStatus` → رشته‌های فارسی `sts_str_fa`.
- `LedgerSourceType` → نوع سند ۱ تا ۴ و ۹.
- **بک‌اند پایه:** معادل کامل `POST /me-data` (ردیف ۶ نقشه) — همه‌ی صفحه‌ها به آن وابسته‌اند.
- **store های Pinia با همان state مرجع:** base، me (از همین حالا با جای `_activeUser` برای تنش ۲)، cart (`useStorage('appCart')` با همان کلید و شکل)، nav، query، token، transition، windowData، report.
- **composable ها:** `getProducts` با فرمول دقیق صفحه‌بندی `floor(count/limit) - 1`؛ موتور تخفیف (۴ نوع، نوع ۳ خاموش، بیشترین `discount_percent`)؛ `getWage`؛ `getHasDiscountWage`؛ علاقه‌مندی خوش‌بینانه.
- **formatter ها با همان خروجی:** ` تومان ` با فاصله‌ها، truncate (نه round) در `useTowDecimal`، حذف علامت در `useFormat`. **validator ها با همان پیام‌ها**، ولی بدون پرچم `g` (نقص #18).
- تست واحد برای همه‌ی این‌ها، با نمونه‌های مرجع.
### Phase 3 — پوسته، ناوبری، انیمیشن
- `App.vue` با `keep-alive`/`suspense`، دو layout، چک نسخه + دیالوگ Lottie، به‌روزرسانی بی‌صدای service worker.
- router با ۲۲ route و guard ها؛ hook آنالیتیکس (وصلش در Batch G).
- ناوبری پایین با GSAP، `appHeader` با shutter، `content` با margin.
- کامپوننت‌های پایه: `DialogVant`، `DialogVantOneButton`، `NotifyVant`، `spinner`، `empty`، `WorkActionSheet`، `container`، `ScrollFetch`، `SearchInput`، `otpInput`، `SingleImageUploader`، `InputDatePicker`.
- هر ردیف جدول انیمیشن با harness فاز ۰ اثبات می‌شود.
### Batch A — ورود، ثبت‌نام، تکمیل پروفایل · حجم بک‌اند: کم
- **صفحه‌ها:** `LoginRegister/index.vue`، `Login.vue`، `Register.vue`، `completeProfile.vue`، `input/otpInput.vue`، `input/SingleOtpInput.vue`.
- **بک‌اند:**
- معادل `POST /major/register` — مشتری تازه با `verificationStep = UNVERIFIED`، با بررسی یکتایی شماره در **هر دو** جدول.
- رد ورود برای `sts_verify_step_one == 3` با پیام «مجوز ورود ندارید» — نگاشتش به وضعیت هدف بررسی می‌شود.
- تکمیل پروفایل و آپلود مدارک: موجود (D22).
- **کوییرک‌هایی که حفظ می‌شوند:** OTP چهاررقمی (APP-01)؛ رنگ notify همیشه قرمز `#DC2626`، حتی برای موفقیت؛ برچسب دکمه «دریافت رمز عبور» / «دریافت مجدد رمز عبور»؛ دیالوگ موفقیت ثبت‌نام که با کلیک بیرون بسته نمی‌شود؛ فیلد شهر متن آزاد؛ تلفن فروشگاه بدون الگو.
### Batch B — کاتالوگ · حجم بک‌اند: زیاد
- **صفحه‌ها:** `home/index.vue`، `search/searchPage.vue`، `search/collection/index.vue` و `CollectionCard.vue`، `newest.vue`، `singleProduct.vue` (صفحه و حالت drawer)، `components/product/*`، `components/search/*`، `components/slider/*`، `viewAll.vue`.
- **بک‌اند:**
- معادل `POST /front`: دسته‌ها با `total`، `icon` و `image`؛ محصولات جدید؛ `more`؛ `ciny`.
- معادل `POST /get-products`: همه‌ی کلیدهای `Query` (`colors`، `brands`، `limit_percent`، `limit_gram`، `has_discount`، `text`، `cats`، `ciny_id`، `page`، `limit`) با پاسخ `{rows, count, limit}`، و حالت `products_id` برای rehydrate سبد. سمت سرور، چون صفحه‌بندی مرجع سمت سرور است.
- معادل `GET /product/{id}` و `GET /ciny/{cat}`.
- معادل `GET /config` (`about` + `main_slider`).
- معادل `GET /promotion` و `GET /ignore` برای کاربر واردشده.
- **کوییرک‌ها:** ترتیب ۹ دسته (Q5)؛ دیالوگ ورود در هر بازدید بعد از ۵ ثانیه؛ ارتفاع اسلایدر `min(width, 768) / 2.2125`؛ تنوع پیش‌فرض = کمترین `percentage_pay` با fallback «بدون تنوع»؛ تخفیف نوع ۳ خاموش؛ «افزودن به سبد» فقط با `sts_verify_step_two === 2`؛ نمونه‌ی تصادفی ۶ کالکشن؛ برچسب «N فیلتر» / «اعمال فیلتر»؛ بازه فقط وقتی «فعال» حساب می‌شود که هر دو سرش عوض شده باشد؛ بنکدار اسلایدر اجرت را نمی‌بیند.
- **وابسته به:** D9 قدم ۵، Q3، Q5، تصمیم دکمه‌ی back.
### Batch C — میز کار (`/user`) · حجم بک‌اند: کم تا متوسط
- **صفحه‌ها:** `views/user/index.vue`، `Dashboard.vue`، `about.vue`.
- **بک‌اند:** تابلوی قیمت (Q4)؛ `grams_sell` برای کارشناس.
- **کوییرک‌ها:** «بدهکار گرمی» / «بستانکار گرمی» با قدر مطلق؛ «فروش گرمی» فقط برای کارشناس؛ کاشی‌ها برای مهمان → دیالوگ ورود؛ دیالوگ «ّکاربر فعال شود؟» با همان تشدید سرگردان؛ دیالوگ خروج؛ متن‌های toast قیمت؛ سرتیتر تاریخ و ساعت قیمت که همیشه خالی است (#7 — کوییرک بصری).
- **تصمیم‌ها:** #5، #6، #8، #19؛ تنش ۲ برای دکمه‌ی «انتخاب مشتری» / «خروج از حساب مشتری».
### Batch D — سفارش‌ها، علاقه‌مندی‌ها، پروفایل، اسناد، گزارش · حجم بک‌اند: زیاد
- **صفحه‌ها:** `Orders.vue`، `product/order.vue`، `product/orderItem.vue`، `favorits.vue`، `profile.vue`، `editUser.vue`، `transactions.vue`، `addTransaction.vue`، `addFindoc.vue`، `report/index.vue`، `report/detail.vue`، `Accounting.vue`، `AccountingDetail.vue`.
- **بک‌اند:**
- علاقه‌مندی: جدول افزایشی، toggle و list.
- ویرایش پروفایل: **فقط فیلدهای فرم** (whitelist). مرجع کل بدنه را روی ردیف مشتری می‌نوشت (`ApiMobileController::updateProfile` — همان خانواده‌ی SEC-14). ستون‌های افزایشی `birthDate` و `marriageDate` (PERS-03).
- اسناد: فهرست به شکل مرجع از `LedgerEntry`. ثبت سند به‌صورت ورودی **تأییدنشده**، با نرمال‌سازی آب‌شده `value × ayar / 750` (همان `REPAYMENT_MELTED_GOLD`). پیامک به مدیران از env، به‌جای دو شماره‌ی هاردکد مرجع (الگوی SEC-15). مرجع اینجا هم کل بدنه را ذخیره می‌کرد.
- فاکتور سفارش و فایل سند (معادل `report/order` و `report/doc`).
- گزارش: موجود (D24).
- **کوییرک‌ها:** ۸ تب وضعیت با همان رنگ‌ها (۰ و ۲ بدون رنگ)؛ ریل چهارمرحله‌ای با `current = 1` پیش‌فرض؛ جست‌وجوی دقیق شماره در کلاینت؛ مودال تمام‌صفحه؛ باز شدن فاکتور در پنجره‌ای به نام `_bank`؛ **دو دکمه‌ی شناور هم‌پوشان** در اسناد، با همان ترتیب لایه‌ها؛ رنگ و آیکون ردیف برای `type == 9`؛ چهار تب فرم سند و متن ثابت تب «پولی»؛ دکمه‌ی «افزودن کاربر» که هنگام کار به‌جای مشتری پنهان است (#11).
- **تصمیم‌ها:** #4، #12، #14، #15.
### Batch E — سبد و ثبت سفارش · حجم بک‌اند: متوسط
- **صفحه‌ها:** `cart.vue`، `product/cartItem.vue`.
- **بک‌اند:** MON-08؛ MON-05؛ پرچم `from_parent` (تنش ۲).
- **کوییرک‌ها:** کلید `appCart` و شکلش؛ حذف بی‌صدای ردیف‌هایی که محصولشان دیگر نیست؛ وقتی تعداد ۱ است، منفی به سطل زباله تبدیل می‌شود؛ drawer یادداشت 280px با textarea پنج‌خطی؛ جمع **فقط وزن** با `.toFixed(2)`؛ دیالوگ تأیید؛ صفحه‌ی موفقیت با شماره‌ی سفارش؛ بنکدار اجرت را نمی‌بیند.
- **تصمیم‌ها:** تنش ۵.
### Batch F — کارشناس فروش · حجم بک‌اند: زیاد
- **صفحه‌ها:** `customerChoice.vue`، `addUser.vue`، `ChildOrders.vue`، و بخش‌های کارشناس در Dashboard و profile.
- **بک‌اند:**
- کار به‌جای مشتری (تنش ۲).
- ساخت، ویرایش و فعال/غیرفعال کردن مشتری توسط کارشناس، با دامنه‌ی `referredByAgentId` و یکتایی شماره در هر دو جدول.
- فهرست نوع مشتری (معادل `admin/major/list_type`) از `CustomerKind`.
- ویرایش سفارش مشتری و ثبت نهایی (معادل `PUT /order/child/{id}`).
- **کوییرک‌ها:** پیش‌پر کردن از `sessionStorage.editCustomer`؛ برگشت به `customerChoice` یا `profile` بر اساس `query.from`؛ ۲۲ منطقه‌ی تهران؛ بنکدار → `sts = -1`؛ toast شماره‌ی تکراری با قرمز `#b10f0f`؛ جست‌وجوی نام در کلاینت؛ ویرایش وزن خام (بدون اجرت)؛ دو متن تأیید.
### Batch G — سفارش ساخت + آنالیتیکس · حجم بک‌اند: متوسط
- **صفحه‌ها:** `makeOrder.vue`، `makeOrderStatus.vue`، کاشی سفارش ساخت در Dashboard، `router/customAnalytics.ts`.
- **بک‌اند:**
- ثبت و فهرست سفارش ساخت مشتری روی مدل موجود `PreOrder` (`colorName`، `sizeSpec`، `minaColor`، `stoneColor`، `description`، تصویر) + ستون افزایشی تعداد — فیلد `sum` مرجع در `PreOrder` نیست.
- تأیید و ارسال توسط کارگاه (تنش ۳، #9).
- آنالیتیکس روی `CustomerActivityLog` + ستون افزایشی `endedAt` (Q6).
- **کوییرک‌ها:** نگاشت ۵ مرحله از رشته‌ی فارسی `sts_str_fa` — سرور باید **همان رشته‌ها** را بدهد؛ `confirmedOrders` فقط در حافظه است و با reload از دست می‌رود؛ کاشی برای type 5 مستقیم و برای بقیه مودال دوگزینه‌ای؛ اسپینری که به‌خاطر سایه‌انداختن متغیر `loading` هرگز دیده نمی‌شود.
### Phase نهایی — ممیزی و تحویل
- همه‌ی ردیف‌های ماتریس: ✅، یا ➖ با شماره‌ی ISSUE، یا ⏸️.
- گزارش مقایسه‌ی تصویری همه‌ی صفحه‌ها × پرسوناها × viewport ها.
- سرشماری‌ها (§معیار تمام شد).
- typecheck، lint، build، `docker compose`، `build:release`.
- به‌روزرسانی `DELIVERY.md` و `PROGRESS.md`.
---
## روش کار هر Batch
1. خواندن **خط‌به‌خط** فایل‌های مرجع آن batch — نه فقط inventory.
2. ردیف‌های ماتریس، فیلد به فیلد.
3. شکاف‌های بک‌اند: endpoint افزایشی + تست (رفت‌وبرگشت، توکن متقابل، IDOR).
4. پورت view ها: template و style تا جای ممکن عین مرجع؛ script بازنویسی‌شده.
5. مقایسه‌ی تصویری و انیمیشن در برابر مرجع.
6. typecheck، lint، build.
7. گزارش کوتاه: چه پورت شد، چه عمداً نشد (با شماره)، چه باگ تازه‌ای در مرجع پیدا شد.
تحت D18 بین batch ها توقف نمی‌شود؛ فقط برای موارد توقف D18 و gate فاز ۰.
---
## معیار «تمام شد» — اثبات خودکار اینکه چیزی از دست نرفت
| سرشماری | چطور | قبول |
|---|---|---|
| route | استخراج path، name و meta از هر دو router | ۲۲ از ۲۲، یکسان |
| رشته‌ها | استخراج هر literal فارسی از فایل‌های زنده‌ی مرجع (template و script) | همه در اپ کلاسیک موجود؛ هر غایب یا در §چیزی که پورت نمی‌شود هست یا خطاست |
| asset | checksum | همه یکسان |
| endpoint | هر ردیف نقشه | ✅ یا تصمیم ثبت‌شده |
| تصویر | مقایسه‌ی هر صفحه × پرسونا × viewport | زیر آستانه‌ی توافق‌شده در فاز ۰ |
| انیمیشن | frame های زمان ثابت | منطبق |
| ماتریس | — | بدون ردیف باز |
| کیفیت | typecheck strict، lint، build، compose | سبز |
---
## چیزی که پورت نمی‌شود (و چرا)
هر مورد «کد مرده» قبل از کنار گذاشتن با جست‌وجوی importer تأیید می‌شود.
| مورد | چرا | شاهد |
|---|---|---|
| `src/views/login.vue` | کل فایل کامنت است | `login.vue:1,201` |
| `user/src/components/report.vue` | صفحه‌ی دیباگ، سوار نشده | `_inv-ref-pwa.md` §6.1 |
| `NotificationList.vue` و سه endpoint اعلان | در مرجع سوار نیست | `home/index.vue:5` |
| FCM (`Mixins/fireBase.js`) و `firebase-messaging-sw.js` | در مرجع غیرفعال؛ کلید API هاردکد | `App.vue:105` |
| پرکردن خودکار OTP (Web OTP API) | غیرفعال | `App.vue:100` |
| `POST /cities/list` | UI آبشاری کامنت است | `Register.vue:58-64` |
| `cax`، `noneAuthUrl`، `canNavigate`، `getHomeRouteForLoggedInUser`، `getCustomerListType`، `loadingStore`، `filters sample.js`، `Date.vue`، `UploadPic.vue`، `updateProfile` بی‌مصرف در `makeOrder.vue`، `logOut` دوم در `user/index.vue` | کد مرده | `_inv-ref-pwa.md` §2.2، §5.1، §8.6، §9 |
| دو fetch تکراری در `singleProduct` | یک fetch؛ نتیجه‌ی دیده‌شده یکسان | #20 |
| `wage_weight` و `weight` ثابت در payload سفارش | سرور نادیده می‌گیرد | DEAD-06 |
| حلقه‌ی `setInterval(reload)` | یک reload | #10 |
| کلید analytics به شکل `undefined` | کلید از نام route | #16 |
| PWA در dev mode | production | #17 |
---
## ریسک‌ها
| # | ریسک | اثر | کاهش |
|---|---|---|---|
| R1 | مرجع روی Node فعلی بالا نیاید (Vite 3، plugin-vue 2، وابستگی `webpack 5.0.0`) | بدون مقایسه‌ی تصویری، «دقیقاً» فقط با خواندن کد ادعا می‌شود | Node قدیمی‌تر فقط در همان کپی؛ اگر نشد، اعلام صریح سطح اطمینان پایین‌تر |
| R2 | تفاوت داده بین دو API | adapter وسوسه می‌شود چیزی جعل کند | قاعده‌ی صداقت؛ تست adapter با fixture |
| R3 | تقویم جلالی | ظاهر متفاوت | spike فاز ۰؛ fallback به fork |
| R4 | حجم بک‌اند | بیشتر کار در `apps/api` است، نه در UI | batch بندی بر اساس وابستگی |
| R5 | رندر فونت بین ماشین‌ها | تفاوت کاذب در مقایسه‌ی تصویری | هر دو اپ روی یک ماشین و یک مرورگر |
| R6 | `keep-alive` | state کهنه بعد از برگشت — که رفتار مرجع است | حفظ، با تست سناریوی برگشت |
| R7 | v1/v2 عقب می‌افتند (`CLAUDE.md` §2) | ناسازگاری منطقی بین اپ‌ها | تنش ۱ |
| R8 | شناسه‌های عددی دسته‌ها | ترتیب اشتباه دسته‌ها در خانه | Q5 |
---
# وضعیت اجرا
هنوز شروع نشده. **قدم بعدی:** پاسخ شما به تنش‌های ۱ تا ۵ و Q1 تا Q9؛ بعد Phase 0.